Closes the long-standing 'jiti picks stale .js over updated .ts' trap
that silently shipped wrong code at least 5 times during sim e2e. Root
cause was three things compounding:
1. tsconfig.plugin.json had outDir=plugin (same as rootDir), so tsc
emitted compiled .js next to .ts sources. jiti's extension resolver
prefers .js, so the moment a .ts changed without a matching rebuild,
the .js won and the new code never ran.
2. The .js artifacts were tracked in git, so even on clean clones the
stale files came back.
3. There was no install script. Every deploy was an ad-hoc tar + rsync
that copied both .ts and .js to the same target dir, recreating the
race on the install side.
This commit fixes all three together:
- tsconfig.plugin.json: outDir=dist/dialectic, module/moduleResolution
flipped from ESNext/bundler to NodeNext/NodeNext (so emitted .js
works under plain Node ESM at runtime, the only thing jiti and the
openclaw gateway actually use).
- .gitignore: /dist/ + plugin/**/*.{js,mjs,cjs,js.map,d.ts}, then
git rm --cached the 4 existing tracked .js files.
- scripts/install.mjs (new, ESM): mirrors Fabric.OpenclawPlugin's
install.mjs pattern — detect, checkDeps, build (clean dist/ first),
install (copy dist/dialectic/ + openclaw.plugin.json + package.json
to ~/.openclaw/plugins/dialectic/), configure (plugins.allow,
plugins.load.paths, plugins.entries.<id>.enabled). Supports
--install / --build-only / --uninstall / --skip-check / --verbose /
--openclaw-profile-path / --backend-url.
Verified on sim dind-t2: install.mjs --install produces a plugin
directory with ONLY .js files (no .ts left behind), gateway loads it,
8 tools register cleanly, dialectic_list_topics + fabric-guild-list
work end-to-end.
Plugin is now fully ESM-clean: type=module + NodeNext + .js import
extensions + no bundler-only knobs. Matches the project-wide invariant
[[project-fabric-esm]] (every Fabric subproject is ESM).
Two changes that fix bug #1 from the first e2e debate run (judge could
not see arguments, returned a hollow 'tie' verdict despite real pro/con
posts being in the DB):
1. New tool `dialectic_list_arguments` — hits the existing backend
endpoint GET /api/topics/{id}/arguments and returns the full
transcript (pro/con/judge entries with camp, agent_id, content,
posted_at) in posted order. Used by judges composing verdicts and
by debaters reading what opponents have said.
2. Updated `dialectic_topic_detail` description to make the split
explicit: it returns the new camps array (added in the matching
backend commit) so agents can locate their own allocation, but
does NOT return arguments — list_arguments is the right tool.
Manifest contracts.tools updated (jiti loader requires this — see
[[reference-meridian-plugin-contract]]) plus internal logger count
bumped from 7 → 8 tools.
E2e verified on sim:
- main agent calls topic_detail → sees camps array with self as 'pro'
- judge agent calls list_arguments → reads pro/con content
- judge rationale directly quotes claims from both sides
- topic → completed with non-trivial binary verdict
Deploy note: jiti prefers .js when colocated; deploy must rm src/*.js
or pre-build before the new .ts takes effect (see e2e findings memory).
Two pre-existing issues surfaced during sim e2e of a full debate:
1. **secret-mgr env injection**: backend-client's resolveApiKey ran
execSync('secret-mgr get dialectic-agent-apikey') without setting
AGENT_ID/AGENT_WORKSPACE/AGENT_VERIFY in the child env. The plugin
process inherits the openclaw gateway env (no agent context), so
secret-mgr refused with 'AGENT_VERIFY mismatch' and the error
surfaced to agents as the generic 'dialectic api key not provisioned'
(stderr was swallowed by stdio:'ignore'). Now we explicitly inject
the pcexec trio plus PATH=~/.openclaw/bin:..., capture stderr so
underlying failures are visible, and use the standard
~/.openclaw/workspace/workspace-<id> layout if AGENT_WORKSPACE
isn't already set.
2. **DIALECTIC_PLUGIN_BYPASS_HF=1 sim escape hatch**: HarborForge's
hasOnCallCovering returns false on sim (sim agents have no real
on_call slots), which blocks dialectic_signup before it ever reaches
the backend. Added an env-gated skip so sim/test environments can
run the full debate flow without provisioning real schedules.
Bypass is opt-in via env, so prod is unaffected.
E2e verified on sim dind-t2 (openclaw) + dind-t3 (backend):
- recruiter/main/simdev minted dialectic-agent-apikey
- propose_topic created topic
- 3 signups all 201
- ticker allocated pro=main, con=simdev, judge=recruiter
- pro+con posted arguments to round 0
- judge submitted binary verdict after debate_end_at, topic→completed
- view_verdict round-trips
Deploy note: jiti loader prefers .js over .ts when both are present in
src/, so updates that only change .ts need the colocated .js removed
(or properly rebuilt) before they take effect. The plugin still ships
src/*.js as pre-built artifacts; consider switching to .ts-only
sources or running 'npm run build' before deploy.
Operator decision: backend env hard-coding a single guild/channel was
wrong. Center can host multiple guilds, each can have multiple
announce channels for different purposes. The proposing agent picks
which one this topic broadcasts to.
dialectic_propose_topic tool schema adds:
- announce_guild_base_url (string, optional)
- announce_channel_id (string, optional)
- both or neither - one-of-two rejected at backend POST time
- omit both - topic has no broadcasts (agents must poll detail)
Tool description points agents at "Fabric channel-list tools" for
candidate discovery. Workflow docs spell out the discovery pattern.
7 tools total now:
- dialectic_list_topics
- dialectic_topic_detail
- dialectic_propose_topic
- dialectic_signup
- dialectic_post_argument
- dialectic_submit_verdict (NEW — POST /api/topics/{id}/verdict)
- dialectic_view_verdict
Also added contracts.tools entry for the new tool, updated README +
startup log line. Sim smoke verified the other 4 (list/propose/signup/
detail) via direct plugin import; submit_verdict not yet smoke-tested
end-to-end (requires running through a full debate to debate_end_at).
Code path is identical to other write tools — bearer + JSON body +
shape-coerced response.
Phase 3 of DIALECTIC-V2. Six tools wired to the Go backend running on
server.t3:
- dialectic_list_topics GET /api/topics
- dialectic_topic_detail GET /api/topics/{id}
- dialectic_propose_topic POST /api/topics
- dialectic_signup POST /api/topics/{id}/signups (HF pre-check)
- dialectic_post_argument POST /api/topics/{id}/arguments
- dialectic_view_verdict GET /api/topics/{id}/verdict
All tools return MCP {content:[{type:text,text}]} shape. Errors caught
and surfaced as text payload so agents see actionable failure messages.
Per-agent API key resolved via secret-mgr key dialectic-agent-apikey
(cached in memory; AGENT_VERIFY env required). HF on_call coverage
check degrades gracefully to skipped if HarborForge.OpenclawPlugin
does not yet expose hasOnCallCovering() — backend stores pre_validated
flag as audit signal.
openclaw.plugin.json declares both contracts.tools (the 6 names) AND
activation.onStartup:true per loader gotchas memory; missing either
silently drops the plugin.
Plain export default {id, name, register} entry shape matching
prism-facet. No openclaw SDK imports (jiti runtime resolution of
openclaw was flaky in HF-style entries; structurally simpler avoids
the lookup entirely).
Defaults backendUrl to https://dialectic-api.hangman-lab.top; override
via openclaw.json plugins.entries.dialectic.config.backendUrl for sim.
Phase 3 deferred items (in README): agent key provisioning workflow,
HF window-coverage accessor, SSE subscription tool, token-cost
reporting.