mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
docs: refresh simplification RFC sweep
This commit is contained in:
@@ -52,9 +52,11 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
|
||||
|---|---|
|
||||
| [Unify the agent id and the session id](proposed/simplification/2026-06-20-unify-agent-and-session-id.md) | 2026-06-20 |
|
||||
| [Narrow the subagent seam to synchronous collect](proposed/simplification/2026-07-04-narrow-subagent-synchronous-collect.md) | 2026-07-04 |
|
||||
| [Drop idle registry observation surfaces](proposed/simplification/2026-07-04-drop-idle-registry-observation-surfaces.md) | 2026-07-04 |
|
||||
| [Prune the bash task roster from the public seam](proposed/simplification/2026-07-04-prune-bash-task-roster.md) | 2026-07-04 |
|
||||
| [Drop idle registry and status observation surfaces](proposed/simplification/2026-07-04-drop-idle-registry-observation-surfaces.md) | 2026-07-04 |
|
||||
| [Remove defaults from the tool-schema DSL](proposed/simplification/2026-07-04-remove-tool-schema-defaults.md) | 2026-07-04 |
|
||||
| [Trim unused hook protocol and bridge surface](proposed/simplification/2026-07-04-trim-hook-protocol-surface.md) | 2026-07-04 |
|
||||
| [Narrow the pre-tool gate to shipped behavior](proposed/simplification/2026-07-04-narrow-pre-tool-gate.md) | 2026-07-04 |
|
||||
| [Fold the stdio UI helper into the stdio app](proposed/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 |
|
||||
|
||||
### Architecture
|
||||
|
||||
@@ -71,6 +73,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
|
||||
| [API extractor reports](proposed/process/2026-06-11-api-extractor-reports.md) | 2026-06-11 |
|
||||
| [Supply chain checks and vendor drift verification](proposed/process/2026-06-11-supply-chain-and-vendor-drift.md) | 2026-06-11 |
|
||||
| [Discover package inventories instead of maintaining static lists](proposed/process/2026-06-20-discover-package-inventory.md) | 2026-06-20 |
|
||||
| [Generate the RFC index tables](proposed/process/2026-07-04-generate-rfc-index-tables.md) | 2026-07-04 |
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
The hook bridges — [`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude) (7 Claude Code hook points) and [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex) (5 Codex points) — map external hook commands onto the harness interception seams. They carry deep unit and coverage-spec coverage (every decision arm, every payload dialect, driven against a mocked seam) plus one key-gated e2e (`hooks.e2e.ts`, a live `PreToolUse` block). But the full-transcript snapshot tier — the one net that boots the real `acp-agent` subprocess, replays a recorded session keyless, and diffs the normalized ACP stdout + re-persisted log against committed goldens — covered exactly ONE hook: a Claude `UserPromptSubmit` block (`hook-prompt-block`).
|
||||
The hook bridges — [`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude) (7 Claude Code hook points) and [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex) (5 Codex points) — map external hook commands onto the harness interception seams. They carry deep unit and coverage-spec coverage (every decision arm, every payload dialect, driven against a mocked seam) plus one key-gated e2e (`hooks.e2e.ts`, a live `PreToolUse` block). But the full-transcript snapshot tier — the one net that boots the real `acp-agent` subprocess, replays a recorded session keyless, and diffs the normalized ACP stdout + re-persisted log against committed goldens — covered exactly ONE hook: a Claude `UserPromptSubmit` block (`hook-cc-promptsubmit-block`).
|
||||
|
||||
That is the tier a mocked unit test structurally cannot be: it exercises the REAL bridge translating a REAL hook process's outcome into the REAL seam decision, then the REAL loop's reaction, rendered exactly as an editor sees it. A bridge-translation or loop-structure regression that left every unit green would still escape it for every hook point but one — and for the Codex bridge, the ACP example did not even LOAD it, so no Codex hook could fire end-to-end at all.
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# RFC: Generate the RFC index tables
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
`docs/rfc/README.md` is hand-maintained even though the repo already has a machine-readable RFC layout: every RFC lives at `docs/rfc/{lifecycle}/{class}/yyyy-mm-dd-topic.md`, and `scripts/verify-rfc-classification.ts` walks that tree to verify structure and index completeness. The current gate prevents drift, but every new RFC still edits the same README tables by hand.
|
||||
|
||||
The stacked hook work made the cost visible. PR #138 added implemented feature/testing/process rows while this simplification sweep added proposed simplification rows, and the only merge conflict when retargeting the sweep onto #138 was the RFC index table. That is predictable: high-churn proposal waves all touch the same few lines even though the truth is already in filenames and H1 titles.
|
||||
|
||||
[The classification RFC](../../implemented/process/2026-06-20-rfc-classification.md) explicitly rejected auto-generating the README index so the file could stay curated. That was a reasonable first cut, but the repo now has enough RFC volume and stacked-PR churn that the hand-written table is the unstable part, not the curated prose. The verifier already does the expensive parsing; it just reports instead of writing.
|
||||
|
||||
## Proposal
|
||||
|
||||
Keep the curated prose in `docs/rfc/README.md`, but generate the per-lifecycle/per-class tables from the filesystem.
|
||||
|
||||
- Add a `gen-rfc-index` script (or extend `verify-rfc-classification.ts` with `--write`) that scans RFC files, reads each H1, derives the first-proposed date from the filename, and writes the table rows under stable generated markers for each `## {Lifecycle}` / `### {Class}` section.
|
||||
- Keep the class set and lifecycle set closed in one script-owned source of truth.
|
||||
- Make `verify-rfc-classification` check that the generated sections are fresh, analogous to `verify-cordis-catalog`.
|
||||
- Preserve manually curated prose, classification descriptions, and "when to write one" guidance outside the generated table blocks.
|
||||
- Update [the classification RFC](../../implemented/process/2026-06-20-rfc-classification.md) to say the earlier "verify, do not generate" choice was superseded after stacked-PR conflicts made the tradeoff worse.
|
||||
|
||||
The generated output should stay boring Markdown: the same tables reviewers read today, just mechanically produced from the path + title source of truth.
|
||||
|
||||
## Why not keep the current verifier-only model?
|
||||
|
||||
The current model catches mistakes but still forces every proposal to edit a shared hotspot. A failed verifier is also more annoying than a generator for a purely mechanical row: the author has already named and placed the file correctly, then has to copy the same facts into the index. That is exactly the kind of hand-maintained inventory the repo already proposes removing elsewhere.
|
||||
|
||||
This does not turn the whole README into a build artifact. The prose remains curated. Only the parts whose content is derivable from RFC files become generated.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `pnpm run gen-rfc-index` (or the chosen command) rewrites only the generated RFC table regions.
|
||||
- `pnpm run verify-rfc-classification` fails when those generated regions are stale and passes after regeneration.
|
||||
- Adding, moving, or deleting an RFC requires editing the RFC file itself; the README rows are produced mechanically.
|
||||
- The generated rows use each RFC's H1 title and filename date, and preserve the existing lifecycle/class grouping.
|
||||
- `pnpm run doc-sync` passes after implementation.
|
||||
|
||||
## Risks
|
||||
|
||||
- Generated regions inside a curated README can be jarring. Use explicit markers and keep the table output minimal so reviewers know what is owned by the script.
|
||||
- Reading H1 titles makes malformed RFC headers a generator concern. That is useful pressure: a missing or nonstandard H1 should fail clearly.
|
||||
- This supersedes an implemented process decision. The implementing PR must amend the old classification RFC so the historical record explains why the tradeoff changed.
|
||||
@@ -1,4 +1,4 @@
|
||||
# RFC: Drop idle registry observation surfaces
|
||||
# RFC: Drop idle registry and status observation surfaces
|
||||
|
||||
Status: proposed
|
||||
|
||||
@@ -12,6 +12,8 @@ Those events carry real complexity. Each registry yields a rollback disposer bef
|
||||
|
||||
There is a related one-shot observation surface in `dsh-llm`: `ctx.llm.models()` returns registered model names, but no production caller uses it. Search finds only service docs and tests, including adapter tests that use it as a registration assertion. The shipped model-call path resolves by `options.model` at `ctx.llm.stream()` time; no UI, router, or product config enumerates model names from the service.
|
||||
|
||||
The same "status without observer" pattern now shows up in the web seam. `ctx.web.searchStatus()` and `ctx.web.fetchStatus()` are documented as diagnostics for `dsh-tool-web`, but the current tools execute directly through `ctx.web.search()` and `ctx.web.fetch()` ([packages/web/tool-web/src/search.ts](../../../../packages/web/tool-web/src/search.ts), [packages/web/tool-web/src/fetch.ts](../../../../packages/web/tool-web/src/fetch.ts)). The execution path already resolves the selected provider at call time and throws a structured `WebError` (`WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_AMBIGUOUS`, `WEB_PROVIDER_CONFIGURED_MISSING`, `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`) when the capability cannot run. The status methods duplicate that selection logic for tests and stale docs, not for a live product surface.
|
||||
|
||||
## Proposal
|
||||
|
||||
Remove the idle registry-observation surfaces that have no production consumer:
|
||||
@@ -20,18 +22,19 @@ Remove the idle registry-observation surfaces that have no production consumer:
|
||||
- Delete `system-prompt/change`, its emits, its JSDoc/README/generated-catalog entries, and listener-throw rollback tests.
|
||||
- Delete `web/providers-change`, its emits, its JSDoc/README/generated-catalog entries, and listener-throw rollback tests.
|
||||
- Delete `LlmService.models()` and update LLM adapter/service tests to assert registration behavior through `stream()` resolution, duplicate-registration errors, disposal, or other behavior that a real caller observes.
|
||||
- Delete `WebService.searchStatus()` / `fetchStatus()` and the `WebCapabilityStatus` contract if no other live type needs it. Web provider `status()` stays internal to provider resolution; callers observe availability by attempting `search()` / `fetch()` and handling `WebError`.
|
||||
|
||||
Registration should remain effect-scoped and HMR-safe: duplicate checks still happen before mutation, the disposer still removes the registered entry, and existing consumers still read the live registry at use time. What disappears is only the speculative observer surface.
|
||||
|
||||
## What stays
|
||||
|
||||
This RFC does not remove live query or execution surfaces. `ctx.tools.schemas()` stays because the system-prompt registry and generated tool catalog use it. `ctx.web.searchStatus()` and `ctx.web.fetchStatus()` stay because `dsh-tool-web` reads them for diagnostics and they share execution-resolution semantics with `ctx.web.search()` and `ctx.web.fetch()`. `ctx.agents.list()`, `ctx.sessions.list()`, and `ctx.sessionPersistence.list()` stay because production code uses them for background-task ownership, invariant seeding, write coordination, and ACP load-cwd validation.
|
||||
This RFC does not remove live query or execution surfaces. `ctx.tools.schemas()` stays because the system-prompt registry and generated tool catalog use it. `ctx.web.search()` and `ctx.web.fetch()` stay because they are the model-facing web tools' execution path and they already carry the provider-selection error taxonomy. `ctx.agents.list()`, `ctx.sessions.list()`, and `ctx.sessionPersistence.list()` stay because production code uses them for background-task ownership, invariant seeding, write coordination, and ACP load-cwd validation.
|
||||
|
||||
This RFC also does not touch live event seams such as `llm/stream`, `tools/execute`, `system-prompt/assemble`, `session/event`, `session/flush`, `agent/status`, or `fs/*`. Those have production listeners or are the documented extension points the architecture depends on.
|
||||
|
||||
## Why not keep them for a future UI?
|
||||
|
||||
A live tool palette, prompt-section inspector, web-provider status panel, or model picker might eventually want registry-change signals. But none exists today, and the current event payloads are so minimal that a real UI would likely need to revisit them anyway. A future observer can reintroduce the smallest signal it actually consumes, with tests that prove the observer sees it.
|
||||
A live tool palette, prompt-section inspector, web-provider status panel, or model picker might eventually want registry-change signals or status queries. But none exists today, and the current event payloads/status shapes are so minimal that a real UI would likely need to revisit them anyway. A future observer can reintroduce the smallest signal it actually consumes, with tests that prove the observer sees it.
|
||||
|
||||
The pre-release stance cuts in favor of narrowing now. A public event with no listener is still API surface; if it survives until release, every later cleanup has to decide whether external consumers might be relying on it.
|
||||
|
||||
@@ -39,12 +42,14 @@ The pre-release stance cuts in favor of narrowing now. A public event with no li
|
||||
|
||||
- `rg "tools/change|system-prompt/change|web/providers-change" packages examples docs --glob '!docs/rfc/**'` finds no remaining declared event, emit, README row, generated-catalog entry, or test outside historical RFC text.
|
||||
- `rg "ctx\\.llm\\.models\\(|\\.models\\(\\)" packages/llm packages/core/agent-loop examples docs --glob '!docs/rfc/**'` finds no remaining `LlmService.models()` API use or docs entry.
|
||||
- `rg "searchStatus|fetchStatus|WebCapabilityStatus" packages/web docs --glob '!docs/rfc/**'` finds no remaining public web status surface, docs entry, generated-catalog entry, or tests except provider-private status concepts that still feed execution.
|
||||
- Registration/disposal tests still prove HMR cleanup for tools, prompt sections/tool providers, web providers, and LLM adapters without depending on observer events.
|
||||
- The [web capability seam RFC](../../implemented/architecture/2026-06-24-web-capability-seam.md), package READMEs, the Cordis catalog, and core data-structure docs are updated to remove the event promises.
|
||||
- The [web capability seam RFC](../../implemented/architecture/2026-06-24-web-capability-seam.md), package READMEs, the Cordis catalog, and core data-structure docs are updated to remove the event and status promises.
|
||||
- `pnpm run test:coverage`, `pnpm run doc-sync`, and `pnpm run hygiene` pass after implementation.
|
||||
|
||||
## Risks
|
||||
|
||||
- Removing emitted events is a public-surface change. The repo is unreleased, and the consumer audit says the current consumers are tests and docs only.
|
||||
- Tests lose an easy way to assert that registration happened. They should assert behavior instead: a registered tool appears in `schemas()`, a registered prompt section appears in `assemble()`, a web provider can be resolved by status/execution, and an adapter can stream for its model.
|
||||
- A future UI may need observer hooks. That is fine; the hook should return with that UI, not ahead of it.
|
||||
- Tests lose an easy way to assert that registration happened. They should assert behavior instead: a registered tool appears in `schemas()`, a registered prompt section appears in `assemble()`, a web provider can execute or throw the expected `WebError`, and an adapter can stream for its model.
|
||||
- Web tests lose a cheap status assertion. They should assert the behavior a real caller observes: successful `search()` / `fetch()` for a usable provider and structured `WebError` codes for unavailable, ambiguous, or misconfigured provider sets.
|
||||
- A future UI may need observer hooks or status queries. That is fine; the hook/query should return with that UI, not ahead of it.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# RFC: Fold the stdio UI helper into the stdio app
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
`@deepseek-ai/dsh-ui-stdio` lives under `packages/support/`, but its only runtime importer is the product app package `@deepseek-ai/dsh-stdio-agent` ([packages/ui/stdio-agent/src/index.ts](../../../../packages/ui/stdio-agent/src/index.ts)). Direct `createStdioChat()` uses are package-local tests and the production wrapper inside the same support package. The examples reach it by loading `dsh-stdio-agent`, not by composing the UI helper themselves.
|
||||
|
||||
That leaves an awkward package boundary. `support/` is documented as lower-compat dev/test/example infrastructure, and the `ui-stdio` README says it is a convenience REPL, not a product surface. But `dsh-stdio-agent` is a shipped app package whose front-door cluster always includes the readline UI, console logger, JSONL persistence, and a pre-created `main` agent. In practice the helper is not an independent swappable capability; it is an implementation detail of the stdio app.
|
||||
|
||||
The boundary adds package metadata, workspace references, generated module-graph rows, README entries, publish lint surface, and a cross-group dependency from `packages/ui/stdio-agent` to `packages/support/ui-stdio`. It also creates a policy mismatch: a product UI app depends on a support package whose docs say it should not be treated as load-bearing product surface.
|
||||
|
||||
## Proposal
|
||||
|
||||
Fold the stdio UI helper into `@deepseek-ai/dsh-stdio-agent`.
|
||||
|
||||
- Move the `createStdioChat` implementation, its `StdioRuntime` test seam, and its unit tests into `packages/ui/stdio-agent`.
|
||||
- Delete the `packages/support/ui-stdio` package, package references, path aliases, dependency entries, module-graph rows, and support README row.
|
||||
- Keep the testable runtime seam inside `dsh-stdio-agent` so EOF handling, rendering, disposal, and piped-vs-TTY behavior remain covered without hijacking process globals.
|
||||
- Update docs that currently point at `../support/ui-stdio` to describe stdio rendering as part of the stdio app.
|
||||
|
||||
After the fold, the stdio app owns its front door the same way `dsh-acp-agent` owns its ACP bridge cluster. The examples still load one app package; no leaf config has to learn a new plugin.
|
||||
|
||||
## Why not promote it to `packages/ui/` instead?
|
||||
|
||||
Promotion would fix the support/product mismatch but keep the extra package boundary. That would make sense if more than one product app composed `createStdioChat()` directly, or if the readline UI were a swappable UI integration in its own right. The current consumer audit says neither is true. The stdio app is the consumer and the owner.
|
||||
|
||||
Re-extraction stays cheap while the repo is unreleased. If a second product app needs the same readline UI independently, split it back out then, with that consumer shaping the package contract.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `rg "@deepseek-ai/dsh-ui-stdio|support/ui-stdio|createStdioChat" packages examples docs scripts --glob '!docs/rfc/**' --glob '!**/lib/**'` finds no deleted package dependency or docs reference; `createStdioChat` remains only as an internal/tested helper under `packages/ui/stdio-agent` if the name survives.
|
||||
- The stdio app still prints transcript events, handles stdin lines/EOF, renders todo updates, and disposes readline listeners under HMR.
|
||||
- Echo/coding-agent keyless smoke tests still boot through the real Loader path and guard the named-export shape.
|
||||
- Package manifests, tsconfig project references, generated module graph, and docs are updated.
|
||||
- `pnpm run test:coverage`, `pnpm run test:snapshot`, `pnpm run doc-sync`, `pnpm run build`, and `pnpm run hygiene` pass after implementation.
|
||||
|
||||
## Risks
|
||||
|
||||
- `dsh-ui-stdio` currently has focused tests with a small package-local setup. Moving them risks blurring app composition tests with UI rendering tests; keep the helper test seam and colocated unit tests to avoid that.
|
||||
- A future standalone terminal UI may want the helper as a package. Reintroduce it when a second product consumer exists rather than keeping a boundary for hypothetical reuse.
|
||||
- Docs that mention the stdio UI as a support example need careful wording so they still distinguish the non-product terminal demo from the ACP product surface.
|
||||
@@ -0,0 +1,45 @@
|
||||
# RFC: Narrow the pre-tool gate to shipped behavior
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The `tools/pre-execute` seam advertises two pieces of deferred capability that are not actually supported end to end: interactive `ask` permission and pre-tool argument rewrite.
|
||||
|
||||
`PreToolDecision` includes `{ kind: 'ask' }`, but `ToolRegistry.execute()` treats every non-`allow` decision as a denied tool result because no permission UI exists yet ([packages/core/tools/src/index.ts](../../../../packages/core/tools/src/index.ts)). The only production producer is `dsh-hooks-claude`, which maps Claude Code `permissionDecision: "ask"` into that variant; Codex has no allow/ask path. The durable hook log can still record that an external hook asked, but the canonical typed seam cannot do anything distinct with it. The public union therefore has a third branch whose runtime semantics are "deny with a different default string."
|
||||
|
||||
The same seam also has an unadvertised argument-rewrite escape hatch. The docs correctly say input rewrite is not offered because `assistant/message`, `tool/call`, and live presentation all see the model's original arguments before execution; changing only `exec.arguments` would make the UI/audit/history disagree with what ran. Yet `ToolExecution.arguments` is mutable, and dispatch reads `exec.arguments` after `tools/pre-execute`, so a listener can rewrite it anyway. A test shim does exactly that to thread a generated bash task id ([packages/bash/tool-bash/tests/integration.spec.ts](../../../../packages/bash/tool-bash/tests/integration.spec.ts)). The proposed [pre-tool input rewrite RFC](../feature/2026-06-30-pre-tool-input-rewrite.md) exists because doing this consistently is a design unit, not a hidden mutation.
|
||||
|
||||
Both shapes are honest feature deferrals, but the public seam currently encodes them as if they were ready. That makes bridge code, docs, generated catalogs, and tests explain behavior whose only shipped result is "deny" or "mutate at your own risk."
|
||||
|
||||
## Proposal
|
||||
|
||||
Make `tools/pre-execute` express the behavior it can actually provide today: allow or deny a pending tool call, without argument mutation.
|
||||
|
||||
- Remove `{ kind: 'ask' }` from `PreToolDecision`. The Claude bridge should still parse and log hook `ask` decisions, but map them to `deny` at the typed seam with an approval-not-supported reason until a real permission prompt exists.
|
||||
- Update docs, generated catalogs, hook bridge README tables, and tests so `tools/pre-execute` is an allow/deny gate, not an allow/deny/ask gate.
|
||||
- Make `ToolExecution.arguments` immutable by contract. At minimum mark it `readonly` and stop relying on a listener-mutated `exec.arguments` for dispatch; if a defensive runtime copy/freeze is needed to make the contract true, add it at the `ToolRegistry.execute()` boundary.
|
||||
- Rewrite the one test shim that mutates `exec.arguments` to use a behavior-level helper instead of the hidden rewrite path.
|
||||
|
||||
When permission prompts or consistent input rewrite lands, reintroduce the smallest explicit decision shape those features need. `ask` belongs with a real user approval loop; argument rewrite belongs with the audit/history/presentation update described by the proposed rewrite RFC.
|
||||
|
||||
## What we give up
|
||||
|
||||
Claude `permissionDecision: "ask"` no longer has a distinct typed-decision branch inside `dsh-tools`. The bridge can still preserve the external fact in `hook/result.decision` and still deny the call conservatively. That matches current product behavior without requiring every native plugin to handle an unusable branch.
|
||||
|
||||
Internal tests lose a convenient mutable-object trick. That is a good loss: public tests should not depend on an unadvertised inconsistency that production docs warn against.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `PreToolDecision` contains only `allow` and `deny`.
|
||||
- `dsh-hooks-claude` still records hook `ask` in hook provenance, but returns a `deny` decision to `tools/pre-execute`.
|
||||
- `rg "kind: 'ask'|PreToolDecision.*ask|ask.*degrades" packages docs --glob '!docs/rfc/**'` finds no remaining public pre-tool ask contract outside historical RFC text.
|
||||
- `ToolExecution.arguments` is no longer a writable rewrite path, and `rg "exec\\.arguments\\s*=" packages examples --glob '!docs/rfc/**' --glob '!**/lib/**'` finds no mutation.
|
||||
- The proposed pre-tool input rewrite RFC remains the future home for a consistent rewrite design.
|
||||
- `pnpm run test:coverage`, `pnpm run test:snapshot`, `pnpm run doc-sync`, and `pnpm run hygiene` pass after implementation.
|
||||
|
||||
## Risks
|
||||
|
||||
- A native plugin author may already have experimented with `ask`. The repo is unreleased, and the branch currently cannot prompt a user; collapsing it now avoids shipping a promise that cannot be honored.
|
||||
- Making arguments immutable may reveal more test helpers that were relying on mutation. Those helpers should move closer to the behavior they actually need instead of preserving a public inconsistency.
|
||||
- Future permission and rewrite work will add back surface area. That is fine; the new surface should land with the product workflow and consistency guarantees that make it real.
|
||||
@@ -8,24 +8,26 @@ The implemented [subagent seam](../../implemented/feature/2026-06-21-subagent-ca
|
||||
|
||||
That means the current start-time capability descriptor is mostly a contract between tests and docs. `SubagentCapabilities.outputSchema` and `toolFilter` are advertised false by every production provider, and the support mock is the only backend that exercises structured output. `depthLimit` is more subtle: the in-process providers advertise it and the shared driver can reject `request.maxDepth`, but no production tool request sets `maxDepth`, so the advertised recursion guard is dormant in the product path.
|
||||
|
||||
The service also exposes registry-observation helpers and lifecycle events that have no production consumer. Grepping `ctx.subagents.getProvider`, `ctx.subagents.list`, `subagent/start`, and `subagent/end` finds declarations, emits, docs, generated catalogs, and tests, but no listener or caller in `packages/*/src` or examples. Keeping those events is not free: `SubagentService.start()` contains custom per-listener dispatch and containment only to protect a run from lifecycle subscribers that do not exist.
|
||||
The #138 hook stack made one earlier simplification idea too broad: `subagent/start` and `subagent/end` are now live. `dsh-hooks-claude` listens to `subagent/start` to run a `SubagentStart` hook and inject any returned `additionalContext` into the live child, and listens to `subagent/end` to run `SubagentStop` ([packages/hooks/hooks-claude/src/index.ts](../../../../packages/hooks/hooks-claude/src/index.ts)). Those lifecycle emits should stay. What remains idle is the registry-observation surface around the provider map: `ctx.subagents.getProvider()` and `ctx.subagents.list()` still have declarations, docs, generated-catalog entries, and tests, but no production caller.
|
||||
|
||||
The result is an over-wide first-cut seam: every provider and every doc page has to explain structured output, tool filtering, depth flags, steering, resume, provider enumeration, and lifecycle telemetry even though the only real product behavior is "start a named child, await its final result, cancel or dispose it."
|
||||
The new hook stack also exposes an overreach inside the lifecycle payload. [The subagent observe-enrichment RFC](../../implemented/feature/2026-06-30-subagent-observe-enrich.md) added `lastAssistantMessage` so a hooks bridge could forward the child output to a `SubagentStop` handler, but the current `SubagentStop` payload builder does not read it; it emits only `agent_id`, `agent_type`, and `stop_hook_active`. The field therefore buys a `structuredClone` branch, clone-failure containment, docs, and tests without changing any shipped hook behavior. If `SubagentStop` should carry the final child message, that should be implemented end to end; until then the payload should be honest.
|
||||
|
||||
The result is an over-wide first-cut seam: every provider and every doc page has to explain structured output, tool filtering, depth flags, steering, resume, provider enumeration, and final-output lifecycle cloning even though the real model-facing behavior is "start a named child, await its final result, cancel or dispose it," plus observe-only lifecycle emits the Claude hook bridge actually consumes.
|
||||
|
||||
## Proposal
|
||||
|
||||
Make the subagent seam describe the behavior the harness actually uses today: synchronous collect only.
|
||||
Make the subagent seam describe the behavior the harness actually uses today: synchronous collect plus the two live observe-only lifecycle emits.
|
||||
|
||||
- Remove `SubagentCapabilities` and the `SubagentProvider.capabilities` field.
|
||||
- Remove `SubagentStartRequest.outputSchema`, `maxDepth`, and `toolFilter`, along with `SubagentService.assertCapabilities`.
|
||||
- Remove `SubagentResult.structured`.
|
||||
- Remove optional runtime methods `SubagentRun.sendMessage` and `SubagentRun.resume`.
|
||||
- Remove the public `SubagentService.getProvider()` and `SubagentService.list()` helpers; provider lookup stays private to `start(name, request)`.
|
||||
- Remove `subagent/start` and `subagent/end` from the Cordis event vocabulary and delete the custom `emitLifecycle` path.
|
||||
- Keep `subagent/start` and `subagent/end`, but narrow their payloads to the fields the live bridge can use: `provider`, `id`, and on end `stopReason`. Remove `SubagentRunEndInfo.lastAssistantMessage`, the `structuredClone(result.output)` branch, and the clone-failure tests/docs.
|
||||
- Remove in-process depth vocabulary that exists only to honor `maxDepth`: `AgentOptions.subagentDepth`, `depthOf`, `SubagentDepthError`, and the child-depth check in `startInProcessRun`.
|
||||
- Update `dsh-subagent-spawn`, `dsh-subagent-fork`, `dsh-subagent-acp`, `dsh-subagent-mock`, `dsh-tool-subagent`, READMEs, [docs/core-data-structures/subagent.md](../../../core-data-structures/subagent.md), and the generated Cordis catalog to the narrower contract.
|
||||
|
||||
After the cut, the provider contract is roughly: `name`, `start(request)`, and a `SubagentRun` with `{ id, result, cancel(), dispose() }`. The start request still carries the load-bearing fields: prompt, parent, optional signal, and optional child agent options.
|
||||
After the cut, the provider contract is roughly: `name`, `start(request)`, and a `SubagentRun` with `{ id, result, cancel(), dispose() }`. The start request still carries the load-bearing fields: prompt, parent, optional signal, and optional child agent options. The service still emits `subagent/start` / `subagent/end` around that run because the hook bridge now consumes them.
|
||||
|
||||
## Why not keep the dormant guard?
|
||||
|
||||
@@ -35,21 +37,25 @@ If a hard recursion limit is needed, it should come back as an actually wired pr
|
||||
|
||||
## What we give up
|
||||
|
||||
Programmatic callers lose prebuilt hooks for structured subagent output, child tool scoping, live steering, follow-up resume, provider enumeration, and lifecycle telemetry. In an unreleased repo, that is an acceptable contraction: none of those hooks has a production caller, and preserving them makes every provider pay an explanation and test cost for speculative behavior.
|
||||
Programmatic callers lose prebuilt hooks for structured subagent output, child tool scoping, live steering, follow-up resume, provider enumeration, and final-output lifecycle telemetry. In an unreleased repo, that is an acceptable contraction: none of those hooks has a production caller, and preserving them makes every provider pay an explanation and test cost for speculative behavior.
|
||||
|
||||
The in-process backends also lose the dormant depth bookkeeping. That does not weaken the shipped model-facing behavior because no shipped request uses it today. It makes the missing recursion policy honest.
|
||||
|
||||
The Claude bridge would no longer be able to forward a child final message to `SubagentStop` without a later payload change. That is also honest: the current bridge does not forward it now. If that behavior becomes product-owned, reintroduce the field with the bridge payload and snapshot/unit coverage that prove the hook sees it.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- The public subagent contract contains only the synchronous collect surface: provider registration, `start(name, request)`, `SubagentRun.result`, `cancel`, and `dispose`.
|
||||
- `rg "outputSchema|structured|maxDepth|toolFilter|sendMessage|resume\\(" packages/subagent packages/support/subagent-mock packages/subagent/tool-subagent docs --glob '!docs/rfc/**'` finds no remaining contract surface except unrelated prose or new historical references.
|
||||
- `rg "subagent/start|subagent/end|getProvider\\(|ctx\\.subagents\\.list\\(" packages examples docs --glob '!docs/rfc/**'` finds no production API surface.
|
||||
- `rg "getProvider\\(|ctx\\.subagents\\.list\\(" packages examples docs --glob '!docs/rfc/**'` finds no production API surface.
|
||||
- `rg "lastAssistantMessage" packages docs --glob '!docs/rfc/**'` finds no live contract, clone branch, test, or generated-catalog entry.
|
||||
- `subagent/start` and `subagent/end` still exist, and `dsh-hooks-claude` still handles `SubagentStart` / `SubagentStop`.
|
||||
- The Cordis catalog, core data-structure docs, package READMEs, and type-equivalence manifest are updated.
|
||||
- Focused subagent tests still prove registration HMR safety, duplicate provider rejection, missing provider rejection, in-process spawn/fork result collection, ACP result collection, abort bridging, and always-dispose behavior.
|
||||
- `pnpm run test:coverage`, `pnpm run test:snapshot`, `pnpm run doc-sync`, and `pnpm run hygiene` pass after implementation.
|
||||
|
||||
## Risks
|
||||
|
||||
- A future subagent UI may want lifecycle events. Reintroduce them with that UI and a payload it actually consumes rather than keeping no-op telemetry now.
|
||||
- A future subagent UI may want richer lifecycle payloads. Keep the live emits now, but reintroduce extra fields only with that UI and a payload it actually consumes.
|
||||
- A future structured-output subagent may want `outputSchema`. Reintroduce it when a provider and consumer both honor it end to end, including validation semantics and model-facing schema design.
|
||||
- A future recursion limit may be necessary. The replacement should be wired through the production subagent tool path instead of relying on an optional field the tool never sets.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# RFC: Prune the bash task roster from the public seam
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The bash executor seam exposes four public background-task operations: direct task lookup via `get(id)`, full roster listing via `list()`, ownership lookup via `ownerOf(id)`, and id-targeted operations `readOutput(id)` / `kill(id)` ([packages/bash/bash/src/index.ts](../../../../packages/bash/bash/src/index.ts)). The model-facing `dsh-tool-bash` consumer uses `start`, `ownerOf`, `readOutput`, `kill`, `onTaskDone`, `run`, and `resolve`, but it never calls `get` or `list` in production.
|
||||
|
||||
The consumer's access policy is deliberately id based. A background task id is returned in the `bash` tool result, then later supplied to `bash_output` or `bash_kill`; those tools compare `ctx.bash.ownerOf(id)` with the calling session token before calling `readOutput(id)` or `kill(id)`. Completion notices also work from a single completed `BashTask` passed through `onTaskDone`, then scan live agents by session owner. None of those flows need a public "show me every task" API.
|
||||
|
||||
Searches for `ctx.bash.get(`, `ctx.bash.list(`, and bash `list(): BashTask[]` call sites outside tests and RFCs find only implementation, docs, generated catalogs, and tests. The local executor still needs its private `tasks` map, but exposing that map as a seam method makes every future bash backend promise roster semantics no current product code consumes.
|
||||
|
||||
## Proposal
|
||||
|
||||
Remove `BashExecutor.get(id)` and `BashExecutor.list()` from the abstract service and first implementation.
|
||||
|
||||
- Delete the abstract methods from `@deepseek-ai/dsh-bash`.
|
||||
- Delete the public methods from `@deepseek-ai/dsh-bash-local`; keep its private task map for `ownerOf`, `readOutput`, `kill`, completion, and disposal.
|
||||
- Update [docs/core-data-structures/bash.md](../../../core-data-structures/bash.md), package READMEs, and the generated Cordis catalog.
|
||||
- Rewrite tests that inspect the roster to assert behavior through returned task handles, `ownerOf`, `readOutput`, `kill`, `onTaskDone`, and disposal.
|
||||
|
||||
The remaining public background contract is direct and smaller: `start()` returns the task handle, `ownerOf(id)` answers the access-policy token, `readOutput(id)` streams incremental output, `kill(id)` stops a known task, and `onTaskDone()` reports completed tasks to interested plugins.
|
||||
|
||||
## Why not keep a roster for UI?
|
||||
|
||||
A UI might eventually show live background tasks. The current seam does not have that UI, and a raw executor-level roster is probably the wrong final surface anyway: a product UI would need task ownership, session routing, presentation state, and maybe persistence or replay. The existing `onTaskDone` callback and tool-result task ids are enough for today's behavior; a future task monitor can introduce an explicit product-facing task inventory if it actually lands.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `BashExecutor` no longer declares `get` or `list`; `LocalBashExecutor` no longer exposes them publicly.
|
||||
- `rg "ctx\\.bash\\.(get|list)\\(|\\.list\\(\\)[^\\n]*BashTask|\\.get\\([^\\n]*BashTask" packages examples docs --glob '!docs/rfc/**'` finds no public seam surface or production caller.
|
||||
- `bash_output`, `bash_kill`, and completion notices still use `ownerOf`, `readOutput`, `kill`, and `onTaskDone` exactly as before.
|
||||
- The Cordis catalog, core data-structure docs, package READMEs, and tests are updated.
|
||||
- `pnpm run test:coverage`, `pnpm run test:snapshot`, `pnpm run doc-sync`, and `pnpm run hygiene` pass after implementation.
|
||||
|
||||
## Risks
|
||||
|
||||
- Programmatic consumers lose an easy way to inspect all tasks. In the unreleased repo, the consumer audit says none exist outside tests.
|
||||
- Tests may become slightly less direct because they cannot assert the private map contents through `list()`. That is a useful pressure: public tests should prove observable behavior rather than pin the executor's storage shape.
|
||||
- A future task dashboard would need a new inventory surface. That should be designed with ownership and UI semantics, not inherited accidentally from an executor map.
|
||||
@@ -0,0 +1,46 @@
|
||||
# RFC: Trim unused hook protocol and bridge surface
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The #138 hook stack added a useful bridge layer, but the current public protocol still exposes a few fields and knobs that no shipped writer or reader uses. They are small individually; together they widen the durable hook log, the shared hook-protocol API, and both bridge configs.
|
||||
|
||||
`HookDialect` includes `'native'`, but real `hook/invoked` writers are the Claude and Codex bridges only. The worked native-plugin test explicitly proves the opposite: a native plugin uses typed Cordis decisions and emits no `hook/*` session events. Grepping `dialect: 'native'` finds a hook-protocol unit test, docs, and type text, not production code.
|
||||
|
||||
`hook/result.durationMs` is durable timing telemetry with no production reader. Both bridges write it; the ACP snapshot normalizer immediately scrubs it to `0` because wall-clock hook runtime is replay noise ([examples/acp-agent/tests/snapshot-normalize.ts](../../../../examples/acp-agent/tests/snapshot-normalize.ts)). The only remaining consumers are tests and generated goldens that exist because the field exists. Persisting a value that replay must erase is a smell: it is neither product behavior nor useful audit state.
|
||||
|
||||
`MergedHookOutcome.systemMessages` is also unused. The codec should still parse `HookOutput.systemMessage` because the external protocols can emit it and both bridges warn when it appears, but the merged aggregate is never read; `rg "systemMessages|\\.systemMessages"` finds the merge helper, README prose, and merge tests only. The bridge already handles warnings per raw output before merge.
|
||||
|
||||
Finally, both bridge configs carry optional process-level defaults that shipped configs do not set. `defaultTimeoutMs` duplicates the reference default (`600_000`) even though each command hook already has its own `timeout`; tests mostly cover schema-bypass fallback. `dsh-hooks-codex` also exposes `Config.model`, but the ACP configs load the Codex bridge with only `configPath`, and every hook payload already has an `Agent` whose `options.model` is the actual model for that run.
|
||||
|
||||
## Proposal
|
||||
|
||||
Remove the unused protocol and config surface while keeping the live external-hook behavior:
|
||||
|
||||
- Change `HookDialect` to `'claude' | 'codex'` until a real native `hook/*` producer exists. Native plugins keep using the typed interception seams directly.
|
||||
- Remove `durationMs` from the `hook/result` session event, `HookResultRecord`, `RunHookResult`, bridge append calls, docs, generated catalog, snapshots, and the snapshot normalizer's special-case scrub. Remove the injected `now` clock from `runHook()` if it becomes unnecessary after the field disappears.
|
||||
- Remove `MergedHookOutcome.systemMessages` and its tests/docs. Keep `HookOutput.systemMessage` parsing and the bridge warnings.
|
||||
- Remove `defaultTimeoutMs` from both bridge configs. Keep per-command `timeoutSec`; when absent, `runHook()` uses a single shared protocol constant for the reference default.
|
||||
- Remove `dsh-hooks-codex` `Config.model`; stamp Codex payloads from `agent.options.model ?? ''` at the point that has an agent, with `''` only for no-agent fallback paths.
|
||||
|
||||
## What stays
|
||||
|
||||
This RFC does not remove `hook/invoked` / `hook/result` themselves. They are live provenance: bridges append them around actual hook execution and ACP snapshots persist them. It also does not remove parsing/warning for `updatedInput`, `systemMessage`, `continue:false`, or `suppressOutput`; those are deliberate faithful-but-degraded external-protocol fields documented by [the hook bridge RFC](../../implemented/feature/2026-06-30-hook-bridges.md).
|
||||
|
||||
This RFC does not collapse the shared `dsh-hook-protocol` package into the bridges or build a single parameterized bridge engine. [The protocol-library RFC](../../implemented/feature/2026-06-30-hook-protocol-lib.md) explicitly keeps only the identical wire primitives shared and leaves per-dialect payload/config mapping in each bridge.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `rg "HookDialect.*native|dialect: 'native'|claude.*/.*codex.*/.*native|claude.*codex.*native" packages/hooks docs/core-data-structures/session.md docs/cordis-catalog/events-and-services.md --glob '!docs/rfc/**'` finds no `HookDialect` branch, test writer, or `hook/*` docs claiming a native durable writer.
|
||||
- `rg "durationMs" packages/hooks examples/acp-agent/tests docs/core-data-structures/session.md docs/cordis-catalog/events-and-services.md --glob '!docs/rfc/**'` finds no hook-result field, snapshot scrub, or generated-golden requirement outside unrelated timing concepts.
|
||||
- `rg "systemMessages|\\.systemMessages" packages/hooks docs --glob '!docs/rfc/**'` finds no merged aggregate surface, while `systemMessage` parsing and bridge warnings remain covered.
|
||||
- `rg "defaultTimeoutMs|Config\\.model|model\\?: string" packages/hooks docs --glob '!docs/rfc/**'` finds no bridge config knob for the removed defaults, while per-hook timeout support and Codex payload model stamping still work.
|
||||
- Hook bridge unit tests and ACP hook snapshots still prove prompt-submit, pre-tool, post-tool, and stop behavior for both dialects.
|
||||
- `pnpm run test:coverage`, `pnpm run test:snapshot`, `pnpm run doc-sync`, and `pnpm run hygiene` pass after implementation.
|
||||
|
||||
## Risks
|
||||
|
||||
- Durable hook timing can be useful diagnostics. If a product UI or trace viewer wants it, add live diagnostics or an intentionally durable telemetry event then; do not keep replay-noisy timing in the base hook-result record without a reader.
|
||||
- A future native hook provenance logger might want `dialect: 'native'`. Add it with that logger. Until then, documenting native hooks as `hook/*` writers blurs the important design point that native plugins do not need the shell-hook log.
|
||||
- A deployment could want a process-level Codex model override for hook payloads. The agent already knows its actual model, which is less surprising than a bridge-level default that can drift from the run being observed.
|
||||
@@ -56,6 +56,9 @@ interface Spawned {
|
||||
stderr: string[]
|
||||
}
|
||||
|
||||
// TODO(acp-test-harness): this subprocess/client boot glue is duplicated with
|
||||
// hooks.e2e.ts and partly with snapshot-harness.ts. Extract one shared ACP test
|
||||
// launcher before the TSX/env/permission-stub details drift again.
|
||||
function spawnAcpAgent(cwd: string, env: NodeJS.ProcessEnv = process.env): Spawned {
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
|
||||
@@ -106,6 +106,9 @@ const SCENARIOS: Scenario[] = [
|
||||
{ name: 'hook-cc-promptsubmit-context', hasModelTurn: true, recorded: true },
|
||||
{ name: 'hook-cc-pretool-deny', hasModelTurn: true, recorded: true },
|
||||
{ name: 'hook-cc-pretool-ask', hasModelTurn: true, recorded: true },
|
||||
// TODO(hook-snapshot-noise): re-record the PostToolUse block fixtures with a
|
||||
// self-limiting prompt or hook so one rejected result proves the seam without
|
||||
// repeated block/retry cycles in the committed JSONL.
|
||||
{ name: 'hook-cc-posttool-block', hasModelTurn: true, recorded: true },
|
||||
{ name: 'hook-cc-posttool-context', hasModelTurn: true, recorded: true },
|
||||
{ name: 'hook-cc-stop-continue', hasModelTurn: true, recorded: true },
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
* Key-gated; owns and disposes its subprocess.
|
||||
*
|
||||
* A keyless companion lives in acp.e2e.ts (stdout purity + session/new); the
|
||||
* full hook-fires-end-to-end transcript is the keyless `hook-prompt-block`
|
||||
* full hook-fires-end-to-end transcript is the keyless `hook-cc-promptsubmit-block`
|
||||
* snapshot scenario. This one closes the "green plumbing, broken product" gap:
|
||||
* only a real model deciding to call bash exercises the PreToolUse seam live.
|
||||
*/
|
||||
|
||||
@@ -57,6 +57,9 @@ type Mode = 'emit' | 'waterfall' | 'parallel' | 'serial'
|
||||
* that manifest documents the `…Map` symbols (`ContentBlockMap`) while
|
||||
* signatures reference the derived UNION names (`ContentBlock`), and it lists a
|
||||
* few symbols on two pages. Here each name resolves to exactly one PRIMARY page.
|
||||
* TODO(catalog-type-links): add a verifier or generator for link-map coverage
|
||||
* so new hook-era decision types like `PromptDecision` / `PreToolDecision` do
|
||||
* not silently appear in signatures without a "Types:" link.
|
||||
*/
|
||||
const LINK_MAP: Record<string, string> = {
|
||||
Agent: 'core.md',
|
||||
|
||||
Reference in New Issue
Block a user