fix review findings: stale coding-agent README, export-shape guards, overclaim wording
Codex review of PR #88 found three issues in the example-app extraction: A1 — examples/coding-agent/README.md's plugin table still listed the OLD direct-wired leaf entries (agent-loop, session-persistence, src/stdio-chat.ts — the whole src/ dir is gone). Rewrite it to the four real leaf entries the current cordis.yml loads (hmr, llm-deepseek, bash, stdio-agent), noting that tool-bash/persistence/agent/loop now live inside the agent-core + stdio-agent bundles. A2 — the three new app/spine packages (agent-core, stdio-agent, acp-agent) export NO `inject`, so a stray `export default apply` would let unwrapExports collapse the module and silently DROP name/Config WITHOUT crashing — the real-load-path smokes would stay green. agent-core is never Loader-unwrapped at all. Add an explicit export-shape guard per package: assert no `default` export and that the real Loader.unwrapExports leaves name/Config/apply intact. Verified each fails when `export default apply` is added. B — soften "structurally unreachable / cannot wire a stdout logger" overclaims in the acp-agent/agent-core READMEs and the implemented RFC: a leaf CAN still add a sibling logger entry; the accurate claim is the app omits one so the default leaf has nothing to get wrong. Keep the safety directive (never add a stdout logger to an ACP leaf).
This commit is contained in:
@@ -41,4 +41,4 @@ The bundle FORWARDS `agent-loop`'s `agents` list as its own (default `[]`), so e
|
||||
|
||||
## Why a code bundle, not a shared YAML include
|
||||
|
||||
A YAML include can dedupe the config, but it cannot OWN a `bin`, and it can only *describe* the front-door coupling in a comment and trust each leaf to obey. Moving the spine into a package, and the front-door cluster into the app packages, turns "the ACP app never logs to stdout" from a prose warning into a property of the artifact. Services register in the root store keyed by their isolate symbol, so a child loaded here is visible to the bundle's siblings (the leaf's adapter and executor) exactly as a nested `plugin-include` subtree's services were — cordis gates every read on `inject`, never on load order.
|
||||
A YAML include can dedupe the config, but it cannot OWN a `bin`, and it can only *describe* the front-door coupling in a comment and trust each leaf to obey. Moving the spine into a package, and the front-door cluster into the app packages, means the default leaf for an ACP server has no logger entry to copy wrong — "the ACP app never logs to stdout" stops being a prose warning a leaf must remember and becomes the app package's default shape (a leaf can still add a sibling logger, so the rule stays documented — but it has nothing to get wrong by default). Services register in the root store keyed by their isolate symbol, so a child loaded here is visible to the bundle's siblings (the leaf's adapter and executor) exactly as a nested `plugin-include` subtree's services were — cordis gates every read on `inject`, never on load order.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import * as agentCore from '../src/index.ts'
|
||||
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
@@ -54,4 +55,25 @@ describe('dsh-agent-core bundle', () => {
|
||||
expect(agentCore.Config).toBeDefined()
|
||||
expect(agentCore.name).toBe('agent-core')
|
||||
})
|
||||
|
||||
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/Config/apply', () => {
|
||||
// Postmortem 0001 guard: a stray `export default apply` makes the Loader's
|
||||
// `unwrapExports` (`exports.default ?? exports`) collapse the module to the
|
||||
// bare `apply` function, DROPPING the named `name`/`Config`. This package has
|
||||
// no `inject` export (it mounts children that carry their own), so that
|
||||
// collapse would NOT crash at load — the plugin would boot but silently lose
|
||||
// its config schema. This bundle is also never Loader-unwrapped by any smoke
|
||||
// (the apps import it directly; the mount test namespace-mounts it), so this
|
||||
// is its ONLY export-shape guard. Assert directly AND through the real
|
||||
// `unwrapExports` so adding `export default` to src/index.ts fails here.
|
||||
expect('default' in agentCore).toBe(false)
|
||||
expect(typeof agentCore.apply).toBe('function')
|
||||
|
||||
const loader = Object.create(Loader.prototype) as Loader
|
||||
const unwrapped = loader.unwrapExports(agentCore) as Record<string, unknown>
|
||||
expect(unwrapped).toBe(agentCore)
|
||||
expect(unwrapped.name).toBe('agent-core')
|
||||
expect(unwrapped.Config).toBeDefined()
|
||||
expect(typeof unwrapped.apply).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ stdout is the ACP JSON-RPC channel, so the cluster is defined as much by what it
|
||||
| ~~console logger~~ | **omitted** — it writes to stdout and would corrupt the protocol frames ([the stdout-purity footgun](../acp/README.md)) |
|
||||
| ~~`hmr`~~ | **omitted** — the editor owns the subprocess |
|
||||
|
||||
Because there is no logger entry in the package, the footgun is **structurally unreachable from the leaf**: a leaf author cannot wire a stdout logger into the ACP config, because the leaf only picks backends, not the front door.
|
||||
Because the package wires no logger entry, an ACP leaf has **nothing to get wrong by default**: it only picks backends, so the common mistake — copying a console-logger entry from the stdio config — has no place here. (A leaf author technically *can* still add `@cordisjs/plugin-logger-console` as a sibling entry; the package can't forbid that. So the rule stands: never add a stdout logger to an ACP leaf — stdout is the JSON-RPC channel. Use a stderr exporter if you need logs.)
|
||||
|
||||
## Config
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import * as acpAgent from '../src/index.ts'
|
||||
|
||||
/**
|
||||
@@ -49,4 +50,24 @@ describe('dsh-acp-agent composition', () => {
|
||||
expect(acpAgent.name).toBe('acp-agent')
|
||||
expect(acpAgent.Config).toBeDefined()
|
||||
})
|
||||
|
||||
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/Config/apply', () => {
|
||||
// Postmortem 0001 guard: a stray `export default apply` makes the Loader's
|
||||
// `unwrapExports` (`exports.default ?? exports`) collapse the module to the
|
||||
// bare `apply` function, DROPPING the named `name`/`Config`. This package has
|
||||
// no `inject` export, so that collapse would NOT crash at load (the keyless
|
||||
// bin smoke would still answer `initialize`) — it would silently lose its
|
||||
// config schema. So guard the shape directly here: assert no `default`
|
||||
// export, and that the real `unwrapExports` leaves `name`/`Config`/`apply`
|
||||
// intact. Adding `export default` to src/index.ts fails this test.
|
||||
expect('default' in acpAgent).toBe(false)
|
||||
expect(typeof acpAgent.apply).toBe('function')
|
||||
|
||||
const loader = Object.create(Loader.prototype) as Loader
|
||||
const unwrapped = loader.unwrapExports(acpAgent) as Record<string, unknown>
|
||||
expect(unwrapped).toBe(acpAgent)
|
||||
expect(unwrapped.name).toBe('acp-agent')
|
||||
expect(unwrapped.Config).toBeDefined()
|
||||
expect(typeof unwrapped.apply).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
import * as stdioAgent from '../src/index.ts'
|
||||
|
||||
@@ -68,4 +69,24 @@ describe('dsh-stdio-agent app', () => {
|
||||
expect(stdioAgent.name).toBe('stdio-agent')
|
||||
expect(stdioAgent.Config).toBeDefined()
|
||||
})
|
||||
|
||||
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/Config/apply', () => {
|
||||
// Postmortem 0001 guard: a stray `export default apply` makes the Loader's
|
||||
// `unwrapExports` (`exports.default ?? exports`) collapse the module to the
|
||||
// bare `apply` function, DROPPING the named `name`/`Config`. This package has
|
||||
// no `inject` export, so that collapse would NOT crash at load (the keyless
|
||||
// echo smoke would still boot the tree) — it would silently lose its config
|
||||
// schema. So guard the shape directly here: assert no `default` export, and
|
||||
// that the real `unwrapExports` leaves `name`/`Config`/`apply` intact. Adding
|
||||
// `export default` to src/index.ts fails this test.
|
||||
expect('default' in stdioAgent).toBe(false)
|
||||
expect(typeof stdioAgent.apply).toBe('function')
|
||||
|
||||
const loader = Object.create(Loader.prototype) as Loader
|
||||
const unwrapped = loader.unwrapExports(stdioAgent) as Record<string, unknown>
|
||||
expect(unwrapped).toBe(stdioAgent)
|
||||
expect(unwrapped.name).toBe('stdio-agent')
|
||||
expect(unwrapped.Config).toBeDefined()
|
||||
expect(typeof unwrapped.apply).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user