Changelog
All notable changes to pm-workspace-kit are documented here.
The format is loosely based on Keep a Changelog, and the project follows Semantic Versioning. Each release also has a longer narrative on GitHub Releases with rationale, dogfood notes, and test plans.
[v0.28.1] — 2026-06-29 — audio long-file fixes (whisper-1, quota refund, monthly cap, diagnosability)
Why
Live testing of a real 45-minute meeting recording exposed three issues the 8-second clip hadn't: long files failed to transcribe, failed attempts silently consumed the daily quota, and the failure reason wasn't recoverable from logs.
Fixed
- Default model →
whisper-1.gpt-4o-mini-transcribeis GPT-4o-based with a ~25-min token cap →400 input_too_largeon long recordings (root-caused live; whisper-1 transcribed the same 45-min / ~5MB file in ~2 min). whisper-1 is bounded only by the 25MB request size, which the 16 kHz-mono re-encode satisfies — consistent with the existing size-based chunking. - Quota refund leak. A single-chunk total failure emitted a gap-marker-only
partialTranscript, which made the coordinator treat it as "cost incurred → don't refund" → reserved minutes leaked (two failed 45-min attempts ate 90 phantom minutes and tripped the daily cap).transcribenow returnspartialTranscriptonly when a segment actually succeeded; total failures refund. Refunds target the reserve-time day/month bucket, so a job crossing a period boundary refunds the file it charged. - Failure diagnosability. The underlying transcribe failure (HTTP status + code, e.g.
400 input_too_large) is surfaced into theaudio.failedevent reason. - Cost estimate.
USD_PER_MINUTE0.003 → 0.006 (whisper-1 list price) soaudio.transcribed.estimatedUsdis accurate.
Added
- Optional whole-workspace monthly budget cap —
audio.quota.globalMonthlyMinuteswith a monthly accumulator file; always tracked (so releases refund it), enforced only when configured.
Verified
- Live: a 45-minute file transcribes end-to-end on whisper-1 (
audio.transcribed durationSec=2736 chunks=1 ms=132359→audio.summarized mode=long). - Code-reviewed (subagent): one MEDIUM (month-boundary refund) found, fixed, and covered by a cross-month test.
Tests
@pmk/cli: 847 tests, 100% pass (+ monthly cap incl. cross-month refund, total-failure-no-partial, failure-detail capture, whisper-1 default).
Notes
- Cost (OpenAI bills in USD): whisper-1 $0.006/min. This deployment runs per-user 600/day, global 1800/day, global 7500/month (≈ NT$1,485).
[v0.28.0] — 2026-06-29 — Slack audio transcription → summary → planning
Why
Let meeting recordings and live audio clips flow through the gateway: upload or record audio in Slack → transcribe → auto-summary with a proactive next-step → continue planning / requirement-clarification in-thread with the uploader.
Added
- Audio pipeline (
packages/cli/src/gateway/audio/) — a detached coordinator (mirrors the:cr:ReviewCoordinator pattern):categoryForroutes audio →claimAudio→🎧 轉錄中…ack → stream-to-temp → ffmpeg probe → too-long gate → quota gate (actual minutes, before any API cost) → OpenAI transcription (ffmpeg re-encode to 16 kHz mono + byte-budget chunking for the 25MB limit) → raw transcript stored as thread context → signal-proportionate summary (short clip vs full PM-structuredlongmode) → posted in-thread. - Config (
audioblock) — dark-launchable (enabled:falsedefault) + first-use consent; OpenAI key via{env}/{cmd}SecretSource (never stored in config);maxDurationSec(2 hr cap); per-user / global daily quota. - Resilience — abort + drain on shutdown (in-flight jobs post a
retrynotice), claim self-heal, secret redaction, prompt-injection framing,retryre-run from the thread.
Fixed
- Audio classification by filename extension (#66) — a voice-memo
.m4aarrived with sparse Slack metadata andcategoryFor(mimetype/filetype only) returned "unsupported", so it never reached transcription. Added anAUDIO_EXTENSIONSfallback on the file name.
Verified
- Live end-to-end: an 8-second clip → transcript → short summary (
audio.transcribed+audio.summarizedevents).
Tests
@pmk/cli: 840 tests, 100% pass.
[v0.19.0] — 2026-06-03 — gateway keep-awake hardening (throttle-proof + self-heal watchdog)
Why
The v0.18.0 demo smoke uncovered a silent multi-day gateway outage: a backgrounded daemon was App-Nap/sleep-throttled on macOS, starving Slack Socket-Mode's ping/pong (5 s window) — the socket reconnected endlessly but never stayed healthy, while the process stayed alive and the heartbeat kept ticking, so nothing looked wrong and the bot answered no one. This release makes that failure mode impossible-by-default and self-healing, and turns any residual unrecoverable state into a loud, alerting exit instead of a silent zombie.
Added
- Throttle-proofing —
keep-awake.ts. On macOS,pmk gateway startnow holds acaffeinatepower assertion bound to its own pid (-w <pid>, auto-released on death) for its whole lifetime, so a backgrounded daemon can't be throttled into starving the socket. Default flags-is(idle+system sleep; deliberately not the heavier-dimsuthat holds the display awake — that remains available viaPMK_GATEWAY_CAFFEINATE_FLAGS). No-op on non-macOS. Best-effort: a spawn failure never blocks startup. - Self-heal watchdog — a pure
SocketHealthtracker (fed by a pong-timeout tap logger + the five real SDK conn-state events) drives aSocketWatchdog(30 s ticks). On a wedged socket (≥3 pong-timeouts/60 s, or notconnectedpast 60 s) it forces an in-process reconnect (time-boxed at 45 s so a hung reconnect can't pin the guard). A reconnect counts as failed only if the socket goes unhealthy again before 3 min of continuous health; after 3 confirmed failures the next unhealthy tick performs a loud exit. - Loud exit —
PresenceBroadcaster.watchdogTerminaterecords agateway.offline(reason: watchdog-unhealthy,broadcast:false— an operator alert, not a stakeholder fan-out), DMs eachconfig.admins(viaconversations.open, over HTTP which works even when the WebSocket is dead) under a hard 15 s cap, thenprocess.exit(1)— guaranteed even if the alert hangs or rejects. With no admins configured, the offline event + terminal log are the alert.
Fixed
- Removed a dead
socket.on("reconnect", …)listener in the Slack adapter (the SDK emitsreconnecting, neverreconnect).
Tests
@pmk/cli: 506 tests, 100% pass. New suites:socket-health,keep-awake,socket-logger,socket-watchdog,socket-watchdog-alert. Listener-survival-across-reconnect verified against the installed SDK source in a holistic review.
Notes
- Host live-sanity (caffeinate child present, clean Ctrl+C shutdown) is an operator check, not covered by unit tests.
- Deferred: launchd/systemd service + boot auto-start; a
watchdogAlertChannelIdfor channel (vs admin-DM) alerts; adaptive thresholds.
[v0.18.0] — 2026-06-03 — adoption metrics + AcmeAds vertical demo (P4 · P5)
Why
Two priorities-plan strands land together. P4 (adoption metrics) answers "is anyone actually using this?" from local signals, so the kit's value can be judged without guessing. P5 (vertical demo bundle) gives a new operator a concrete, self-contained way to watch the knowledge loop work end-to-end on a fictional ad-tech workspace (AcmeAds), rather than reasoning about it abstractly.
Added
pmk adoption(P4) —packages/cli/src/adoption.ts. A pure report builder over the audit window, telemetry sidecar, atom corpus, and run markers (run-markers.ts,~/.pmk/adoption.json): five clamped metrics (first-run / first-PRD markers, atom reuse rate, questioned rate, active-window turns).- AcmeAds demo content (P5a) —
acme-ads-seed.ts: five approved AcmeAds atoms (ad placements, vCPM, customer migration, finance terms, an onboarding dedup rule), taggedacme-ads-demo, seeded/unseeded idempotently viapmk demo seed/unseed. pmk demo run(P5b) —commands/demo.ts+demo-runner.ts: posts the five guided questions as a real user (one-timePMK_DEMO_USER_TOKEN,chat:writeonly), correlates eachturn.processed, and prints a Q→A transcript. Reply-readback uses the gateway bot token'sconversations.replies(bot replies are always threaded).--dm/ zero-config DM auto-open targets the bot DM (usesim:history);--channeltargets a channel (needs the bot'schannels:history).--dry-runpreviews.- AcmeAds demo walkthrough (P5c) —
examples/acme-ads-demo.md: a ~15-minute "watch the loop" guide, leading with the zero-credential manual DM path.
Fixed
- Demo
readReplyno longer silently swallowsconversations.repliesfailures — a missing bot scope (e.g.missing_scopeforchannels:historyin a public channel) now surfaces in the transcript instead of a generic "did not stabilise".
Verified
- The knowledge loop was verified live end-to-end on the host: a
pmk demo runagainst the seeded AcmeAds corpus produced fiveturn.processedevents with the correct atoms injected, the right audience tier, and the Q5 escalation boundary (hadMraAsk), with grounded answers. The automated transcript-capture additionally depends on the bot's reply-read scope (im:historyfor DMs /channels:historyfor channels) and on Slack delivering the demo user's messages — documented in the walkthrough's troubleshooting note.
Tests
@pmk/cli: 483 tests, 100% pass.
[v0.17.0] — 2026-05-30 — atom usage telemetry (P2a)
Why
As the PKB grows, approved atoms need to stay high-signal and low-signal atoms need to become visible and removable (priorities-plan P2). That judgment can't be made without data on which atoms actually get reused and which get questioned after they're cited. v0.17.0 ships the telemetry instrumentation — the measurable foundation. The approver rubric + quarterly audit playbook (P2b) are deliberately deferred until real telemetry accumulates, so the rubric isn't written in a vacuum.
Added
- Atom telemetry sidecar —
packages/cli/src/gateway/atom-telemetry.ts.~/.pmk/gateway/atom-telemetry.jsonis the authoritative per-atom counter store (reuseCount,lastRetrievedAt,questionedCount,lastQuestionedAt). Bumps are synchronous (race-free under the daemon's parallel turns — a sync load-modify-save runs to completion before any other callback interleaves) and crash-safe (temp-file + rename), and are failure-isolated so telemetry can never break a turn or a reaction. The dedupe ledger is bounded (QUESTIONED_KEYS_CAP). - Citation linkage on
turn.processed— the event now carries optionalatomIds/channelId/threadTs/replyTs, so a later 👎 or escalation can be mapped back to the atoms a reply cited. Atom.mdfiles are never touched (the BM25 mtime index is never invalidated by telemetry). pmk gateway atoms telemetry [--json]— joins the sidecar with the approved corpus, lists each atom's reuse/questioned counts + age, sorted weakest-first, flagging dead-weight (never reused) and load-bearing (high reuse, no questions). Kept separate fromgateway audit(runtime-health window) on purpose.
Instrumentation
- Reuse is bumped at LLM success (next to the
turn.processedemit), never on a failed turn. - Questioned is bumped from a 👎 (
-1/thumbsdown) on a cited reply (xstays reserved for pending-atom approval-reject) and from an escalation in a turn that injected atoms. Both paths dedupe (reaction:…/escalate:…keys) against Slack retries / re-reactions.
Tests
@pmk/cli: 446 → 459, 100% pass. Newgateway-atom-telemetrysuite (sidecar, dedupe, cap eviction, runner wiring, report builder) + reaction/event coverage. Verified live on the host daemon: a cited DM bumpedreuseCountto 1 and surfaced inatoms telemetry.
Deferred (P2b)
- Approver rubric ADR + quarterly atom audit playbook — gated on accumulated real telemetry.
questionedKeyspruning, telemetry-as-ranking-input, and agateway auditsummary line are future work.
[v0.16.0] — 2026-05-29 — gateway onboarding (manifest · doctor · dry-run · demo seed)
Why
Through v0.15 the gateway worked, but standing one up meant reading the source: init recited OAuth scopes line by line, there was no way to tell whether tokens / keys / mra workspace were actually wired before the daemon hit Slack, and the only way to test the retrieval → LLM → escalation path was to post real messages into a real channel. PRD-2026-0006 reframes the goal to one sentence: a clean machine, without reading source, gets a bot answering in ~30 minutes. v0.16 ships the five onboarding surfaces (FR1–FR5) plus the M6 baseline trial that the PRD's quality gate requires.
Added
- Slack app manifest (FR1) —
packages/cli/src/gateway/slack/manifest.template.jsoncarries every required bot scope + event subscription.MANIFEST_VERSION = "2026-05"lives inmanifest-version.ts(deliberately not inside the JSON, so Slack's schema doesn't reject the upload).gateway initnow prints the manifest path +api.slack.com/apps?new_app=1instead of reciting scopes. pmk gateway doctor(FR2) — 8 read-only preflight checks (config-file mode 0600, Slack app + bot tokens via liveauth.test/apps.connections.open, Anthropic echo, mra workspace, PKB content, channel ACL, manifest alignment). FAIL → exit 1;--jsonfor CI / hooks. Each check is its own sub-module underdoctor-checks/so new failure modes add a file, not a branch.pmk gateway start --dry-run(FR3) — wraps the SlackWebClientat the outermost layer (dry-run-wrapper.ts); every write (chat.postMessage/postEphemeral/reactions.add) is intercepted, logged as a stub, and never sent. Events route todryrun-events-YYYY-MM.log; Ctrl+C prints session metrics. Interception is centralized so no caller can forget to skip.pmk gateway demo seed|unseed(FR4) — seeds onesource: "demo-seed"atom + ademo-onboardingchannel allowlist entry so a new host can smoke-test the full retrieval chain, thenunseedremoves exactly what it added (symmetric, no residue).- Onboarding guide (FR5) —
gateway/onboarding.md: a 30-minute sequence (manifest → tokens → init → doctor → demo seed + dry-run → go live), cross-linked with the README andgateway/lifecycle.md.
Hardened (M6 trial finding)
pkb-contentnow FAILs on an empty PKB whose source can't fill it. The M6 four-failure trial found the empty-PKB mode slipped through: the check only inspected config shape, so a gateway pointed at an mra workspace with 0 repos passed as a soft WARN anddoctorexited 0. It now counts approved atoms on disk (read-only, newatomCountrunner defaulting toapprovedAtomCount()) and FAILs only when the source provably can't seed it (mra source with 0 repos / unreachable workspace, ormra:ingest with nomraWorkspace). A fresh-but-viable install (0 atoms, repos ≥ 1) stays a WARN — no false positive on clean installs.llm-providercheck (wasanthropic-key) recognizes the claude-agent OAuth path. The check tested only a raw Anthropic API key and FAILed (exit 1) when none was set — butresolveProviderfalls back to the localclaudelogin (claude-agent SDK / OAuth) when no key is present, so doctor was false-FAILing a valid OAuth-only host and blocking startup. It now mirrors the runtime resolution (PMK_PROVIDER→ CLIconfig.provider→auto): in auto mode a key wins (echo-verified), else a presentclaudebinary PASSes, else FAIL. NewclaudeClirunner +llmProvideronDoctorContext.
M6 baseline (PRD-2026-0006 §9 quality gate)
The four runtime-blocking failures the gate demands we deliberately provoke — expired App-Level Token, nonexistent mra workspace, empty PKB, stale manifest — were each driven through runDoctor against the real checks:
| Failure mode | doctor verdict | exit | actionable hint |
|---|---|---|---|
| Expired App-Level Token | FAIL slack-app-token | 1 | regenerate at api.slack.com → App-Level Tokens |
| Nonexistent mra workspace | FAIL mra-workspace | 1 | verify path + .collab/repos.json |
| Empty PKB (0 atoms, 0 repos) | FAIL pkb-content | 1 | register repos, re-run doctor |
| Stale manifest (missing scope) | FAIL manifest-alignment | 1 | add the named scope to oauth_config.scopes.bot |
- Doctor coverage: 4/4 (100%). Pre-hardening it was 3/4 — empty PKB was the gap, now closed. The trial also surfaced (and fixed) the
llm-providerOAuth false-FAIL above. - Live preflight, real environment → ready, exit 0.
pmk gateway doctorrun against the maintainer's actual OAuth-only setup: real Slack tokens (teamslack-webhook), 63 mra repos,PKB has 1 approved atom, andllm-providerPASS — "no API key set — will use local claude login (claude-agent SDK)". 7 pass / 1 warn (DM-only) / 0 fail. NoANTHROPIC_API_KEYneeded: the gateway runs on the host's existingclaudelogin. - Live first-message turn confirmed (real host, OAuth, no key). A DM to the running production bot drove the full path end-to-end: socket-mode receive → retrieval (
atomsInjected: 1) →audience: tech(per-user override) → LLM turn via the localclaudelogin → reply posted to Slack ("在線,v0.16 gateway-DM 就緒。") +reactions.add, withturn.processedlogged server-side. This is the decisive proof that the runtime LLM path needs no API key — the doctorllm-providerPASS is backed by a real turn. - Time-to-first-message: deferred, not faked. The metric measures how long a fresh operator who hasn't seen the source takes — a number neither the maintainer (knows it cold) nor an automated agent (unrealistically fast, non-representative) can produce honestly. Baseline is deferred to the first real external onboarding rather than recording a misleading figure. See the PRD metrics note.
Tests
@pmk/cli: 371 → 446, 100% pass. New / extended suites:gateway-manifest,gateway-doctor(every check PASS + FAIL path, including the new empty-PKB atom-count branches and the llm-provider auto/anthropic-api/claude-agent matrix),gateway-dry-run,gateway-demo-seed.
Out of scope / backlog
- Polished AcmeAds demo bundle — priorities-plan P5. M4 ships only a smoke-test seed.
- Atom quality rubric / telemetry — priorities-plan P2, gated on this baseline.
- Doctor auto-fix — PRD §4 non-goal; doctor stays read-only forever.
[v0.15.0] — 2026-05-21 — workspace-configurable audience domain examples
Why
v0.13.3 expanded the BIZ jargon cheat-sheet to 9 rows + 2026-05-20's v0.14.0 added a meta-rule reframing the cheat-sheet's last row (AdFormat / placement / inventory) and the PM example table (vCPM formula, placement_id, PlacementRevenue vs AccountPayable) as "from an ad-tech reference workspace; apply the discipline to your own domain". The reframe stops the bot from inventing ad-tech context unprompted, but it doesn't teach non-ad-tech workspaces what their own domain vocabulary should translate to — operators ended up either tolerating English terms in body text or forking the shared package to swap in domain-specific rows.
v0.15.0 closes that loop with a runtime extension point: cfg.audience.domainExamples.{biz,pm} carries per-workspace { techForm, targetForm } rows that pickGatewayPrompt(audience, extras) appends to the BIZ / PM cheat-sheet at assembly time.
Added
pickGatewayPrompt(audience, extras?)(packages/shared/src/index.ts) — back-compat default (no extras → returns the base const unchanged for v0.7+ callers). With BIZ extras: appends aWorkspace-specific termsblock + aTech form | Plain-Chinese formtable. With PM extras: appends aWorkspace-specific examplesblock +Tech form (don't ask this way) | PM form (ask this way)table. Tech / exec tiers ignore extras (no translation tables in those prompts).AudienceConfig.domainExamples(packages/cli/src/gateway/config.ts) —{ biz?: DomainExample[]; pm?: DomainExample[] }.loadGatewayConfigback-fills the field on read so existing v0.7–v0.14 configs upgrade silently.pmk gateway audience example add|remove|list(CLI) and/pmk admin audience example add|remove|list(Slack) — admin commands to register translation rows at runtime. Slack syntax uses=as the separator so multi-word target forms survive whitespace tokenisation (/pmk admin audience example add biz tenant = 客戶 / 訂閱戶). Both surfaces re-key bytechForm— adding a row that already exists updates instead of duplicating. Audit log recordsaudience.example.add/audience.example.removeactions.- Wired through at both prompt-assembly points:
FreeChatTurnRunner(packages/cli/src/gateway/slack/free-chat-turn.ts) andEscalationCoordinator(packages/cli/src/gateway/slack/escalation.ts) now callpickGatewayPrompt(audience, config.audience.domainExamples).
Tests
@pmk/shared24 → 29 —pickGatewayPrompt(audience, extras)covered for: no-extras back-compat, BIZ table append, PM table append, tech/exec ignore extras, wrong-tier slot ignored.@pmk/cli362 → 371 — Slack adminaudience example add/remove/listcovered for: append, multi-word target rejoin (= 客戶 / 訂閱戶), duplicate-techForm update-not-duplicate, unsupported-tier rejection, missing-=rejection, remove-not-found idempotency, list render,loadGatewayConfigback-fill from old shape.- Total: 412 → 426 tests, 100% pass.
Operator note
Same in-memory snapshot caveat as the rest of audience: edits via Slack admin or CLI write through to ~/.pmk/gateway.json, but the running daemon keeps its in-memory snapshot until graceful restart:
kill -TERM $(cat ~/.pmk/gateway/gateway.pid) && pmk gateway start
Within the v0.11 marker window (5 min) the restart stays silent in Slack.
Out of scope / backlog
- Per-channel example overrides — currently examples are workspace-scoped. If a host runs one Slack workspace that spans multiple distinct product domains (e-commerce in
#shop, ad-tech in#ads), there's no way to tier examples by channel yet. Deferred until a real multi-domain workspace surfaces the need. - TECH-tier examples — the tech prompt has no cheat-sheet to extend; if dogfood shows tech-tier replies also imprinting on ad-tech anchors, the next iteration would add a tech-specific addendum section. Not yet motivated by observed behaviour.
[v0.14.0] — 2026-05-20 — SlackAdapter tranche 4 + post-v0.13.3 docs/prompt polish
Why
Closes out the v0.13 SlackAdapter decomposition arc started in v0.13.0 (tranches 1–3: presence / envelope-dedup / concurrency / escalation / free-chat-turn / inflight-queue). After tranche 3 the adapter shell sat at 1043 lines — over the <800 dispatcher-cleanup target set during v0.13 planning. Tranche 4 ships two more focused coordinators and folds in two small post-v0.13.3 backlog items that surfaced while sweeping for release.
Fixed / Refactored
slack/index.ts1043 → 734 lines (closes the<800target). Adapter shell now == lifecycle (start / waitForPending / stop) + event-routing glue (handleMessage / handleAppMention / handleDmMessage / handleReactionAdded / handleSlashCommandEnvelope) + the ambient Slack-event types. Behavior unchanged; 412 / 412 tests still pass.slack/slash-command.ts(new, 215 lines) —SlashCommandHandlerowns/pmk <verb>dispatch (help/open/show/close/cases/admin). Pure-helperslashCommandArgsFromBodyandSlashCommandScope/SlashCommandArgstypes move with it;slack/index.tsre-exports them so existing test imports (gateway.test.ts) keep working. Envelope-level glue (ack / dedup / blocklist / error logging) deliberately stays onSlackAdapter.handleSlashCommandEnvelope— those concerns are shared with every other Slack event type.slack/channel-mention.ts(new, 180 lines) —ChannelMentionHandlerowns channelapp_mentionrouting (slash forward / free-chat / case-mode LLM round + tracking summary).SlashCommandHandlerandFreeChatTurnRunnerinjected via constructor so the dependency graph stays explicit.#1 channels-override docs catch-up— README quick-start andlifecycle.mddeep-dive were missing theset-channel/unset-channeladmin surface even though the feature shipped in v0.11.0 (#23) with full CLI + Slack admin coverage and 9 tests. Three small doc patches catch them up; resolution order at turn time now documented inline (per-user → per-channel → workspace default).#3 PM/BIZ ad-tech reframe— BIZ jargon table (AdFormat/placement/inventory) and PM example table (vCPMformula,placement_id,PlacementRevenuevsAccountPayable) were lifted from real OneAD-like dogfood. Both tiers now lead the relevant block with a workspace-context meta-rule: "the examples below are from an ad-tech reference workspace; apply the same translation discipline to your domain's terms — do not citeAdFormat/placementliterally when they don't appear in the workspace." Workspace-configurable domain examples (cfg.audience.domainExamples) deferred to v0.14 backlog.
Tests
@pmk/cli 362 → 362 (unchanged — refactor preserves behavior). @pmk/shared 24 → 24 including the "audience prompts have distinct bodies (cross-wire regression)" test which still passes after the BIZ + PM meta-rule additions.
Live-Slack verify
Verified end-to-end on a real Slack workspace before tagging:
- DM
/pmk help→handleDmMessage→slashCommand.run({scope: user})— bot reply matchescase "help"text exactly. - Channel
@pmk /pmk help→handleAppMention→channelMention.run→ slash forward →slashCommand.run({scope: channel})— bot reply matchescase "help"text exactly. Confirms ChannelMentionHandler's/pmkforwarding branch. - Channel
@pmk <free-chat>→channelMention.run(no slash prefix, no activeCase) →freeChatTurn.run— bot answered per prompt;turn.processedevent emitted (audience: tech,hadMraAsk: false). - BIZ-tier reframe (temporarily removed user override to hit workspace default
biz) — asked a non-ad-tech permission-control question; bot returned a 3-layer structural answer applying the cheat-sheet translations (role→ 角色,scope→ 歸屬關係 二次過濾) with zero ad-tech leak (AdFormat/placement/inventorynever appear). Workspace-specific OneAD subsystem names (ODM/OAM/SuperDSP) DID appear because they are real workspace context, which is exactly what the reframe wants — meta-rule blocks unprompted cheat-sheet jargon insertion, not legitimate context references.
Operator note
This is a refactor + prompt + docs release. No config migration needed. The gateway picks up new code on next graceful restart:
git pull && npm run -w @pmk/cli build
kill -TERM $(cat ~/.pmk/gateway/gateway.pid) && pmk gateway start
Backlog (deferred to v0.14.x)
- Workspace-configurable domain examples —
cfg.audience.domainExamplescarrying per-workspace translation pairs + example questions, injected into BIZ/PM prompts at assembly time. Enables ad-tech workspaces to keep concreteAdFormat/placementanchors while non-ad-tech workspaces get their own domain vocabulary. Punted from v0.14.0 because the current single-workspace dogfood doesn't justify the new config surface + admin commands yet.
[v0.13.3] — 2026-05-20 — gateway audience prompts: anti-bleed + BIZ translation table
Why
v0.13.2 made biz the default — verification showed the biz reply was dramatically cleaner than pm (zero code blocks, plain-Chinese structure, business-meaning section). But two follow-ups from the same dogfood round:
- PM tier was leaking code blocks. A 4-line Ruby
def/loop snippet appeared in the PM-tier reply to "admin 權限的核心邏輯", even though the PM prompt's existing rule said "No code blocks unless quoting an exact API name or table column". Root cause: prior turns in the channel were tech-tier replies with full Ruby blocks; the LLM was tone-matching the conversation history, not strictly following the system prompt. - BIZ translation cheat-sheet was thin. Only 3 examples inline (
AdFormat→ 廣告版型,scope→ 篩選條件,AASM→ 狀態機). For Rails-stack workspaces the bot will seeDevise/Doorkeeper/Rolify/CanCanCan/Sidekiq/migrationetc. all the time; without explicit translations the LLM either keeps the English term or invents inconsistent translations across turns.
Fixed
- PM prompt: explicit "no multi-line code blocks" rule (
packages/shared/src/index.ts). The existing "No code blocks unless quoting an exact API name or table column" stays, but the new wording forces inline-backtick-only for names (PlacementRevenue,app/services/foo.rb,is_admin_permission?) and demands prose description instead of multi-line snippets: "Ability 用 dynamic dispatch — Rolify 表裡的角色名稱被當 method 名稱呼叫" replaces the 4-line Ruby loop. - PM + BIZ: anti-bleed guardrail. Both prompts now lead with the same first rule: "Your tier dictates style, not the conversation history. If prior turns in this thread used multi-line Ruby/SQL blocks, do not tone-match — they were for a different reader." This counters the LLM's default behaviour of mirroring conversation tone when the audience tier silently changes mid-channel.
- BIZ: 9-row jargon translation table (was 3 inline examples). Covers common web-stack terms:
Devise/Doorkeeper/OAuth/JWT → 登入機制 / 第三方授權登入,Rolify/role → 角色管理,CanCanCan/ability.rb→ 權限規則,scope→ 資料篩選條件,AASM/state machine → 流程狀態管理,Sidekiq/cron → 背景排程,migration→ 資料表結構變更,controller/endpoint → 後台處理動作,AdFormat/placement → 廣告版型. Bot is explicitly told to never leave the tech form alone in body text. - BIZ: explicit "對業務的實際意義" section convention. When the answer has operational implications, the reply ends with a 1–3 bullet section spelling out what the user should know (matches the pattern observed in the v0.13.2 verification round).
Tests
@pmk/cli 362 → 362 (unchanged). Shared package's "audience prompts have distinct bodies (cross-wire regression)" test still passes — the four prompts are still distinct after the edits. All 412 workspace tests still pass.
Operator note
This is a prompt-only change inside @pmk/shared. The gateway picks up new prompts on next start — no config migration needed. To apply on an existing host:
git pull && npm run -w @pmk/cli build
kill -TERM $(cat ~/.pmk/gateway/gateway.pid) && pmk gateway start
The anti-bleed guardrail's effectiveness is measurable: if you observe a PM-tier or BIZ-tier reply that still includes a multi-line code block AFTER this upgrade, file an issue with the Slack thread URL — the prompt isn't strong enough yet and we need another iteration.
[v0.13.2] — 2026-05-20 — gateway audience: re-flip default pm → biz
Why
v0.13.1 flipped defaultAudience() from tech → pm on the assumption that PM tier was the right "middle ground" for unknown / non-IT users. Live re-verification on a real workspace (2026-05-20, ~30 min after v0.13.1 shipped) showed the gap between tech and pm was too narrow for the actual product intent: PM tier still cites file paths (app/models/ability.rb), model names (PlacementRevenue), and method names — by design, because PM tier is for "PM who briefs engineers later". A true non-IT user (sales / ops / exec-adjacent) still got a code-flavoured reply with a stray Ruby block that the PM prompt's own "no code blocks unless quoting an exact API name or table column" rule was supposed to ban.
biz is the actual right default. The BIZ prompt forces jargon translation (AdFormat → 「廣告版型」, scope → 「篩選條件」, AASM → 「狀態機」), bans code blocks unless quoting operational SQL the user must run, and collapses implementation into "想看實作可以再問 IT". For the average pmk-gateway workspace this matches what the operator actually wants: non-IT stakeholders get plain-Chinese answers, IT/PM users opt in to the richer tiers explicitly.
Fixed
defaultAudience()returns{ default: "biz" }(packages/cli/src/gateway/config.ts). Same factory-only effect as v0.13.1 — existinggateway.jsonfiles are not modified on upgrade. PMs and engineers opt in via per-user override:pmk gateway audience set <PM-USER-ID> pmpmk gateway audience set <IT-USER-ID> tech- Test assertion follows the factory — same back-fill test in
gateway.test.tsflipped fromdefault === "pm"todefault === "biz"; intent unchanged ("legacy configs back-fill with the default").
Tests
@pmk/cli 362 → 362 (unchanged). All 412 workspace tests still pass.
Operator note
If you're upgrading from v0.13.1 (or any earlier version) and want non-IT users to get BIZ-tier replies:
pmk gateway audience default biz
# Optionally add PM-aware users (will still get file/model refs they can forward):
pmk gateway audience set <PM-USER-ID> pm
# Restart gateway so the in-memory config snapshot updates:
kill -TERM $(cat ~/.pmk/gateway/gateway.pid) && pmk gateway start
The pm and tech tiers still exist and work — only the default changed.