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.
[v0.13.1] — 2026-05-20 — gateway audience: flip default tech → pm
Why
Same-day dogfood on v0.13.0 surfaced a product mismatch the audience-tier code has carried since v0.8.0: defaultAudience() returned { default: "tech" }, so every unknown / unconfigured user got engineer-grade replies with file paths, API names, and :-separated module references. A test channel with a non-IT teammate (PM / ops) saw the bot answer "ability.rb 沒有全域 admin 角色" with file-line refs and a dynamic dispatch callout — accurate but unusable for the actual reader. The four prompts (tech / pm / biz / exec) already differentiate cleanly; the bug was that the default flipped the wrong way for a typical pmk-gateway workspace where most stakeholders are non-IT.
Fixed
defaultAudience()returns{ default: "pm" }(packages/cli/src/gateway/config.ts).pmis the middle tier: structural findings without formulas, jargon translated (AdFormat→ 「廣告版型」,scope→ 「篩選條件」,AASM→ 「狀態機」), PM-framed questions back to the user. IT users opt in via explicit per-user override (/pmk admin audience set <@user> techorpmk gateway audience set <userId> tech).- Existing installs are unaffected on disk — the factory only fires when
audienceis missing fromgateway.json. Upgrading admins who want the new behaviour can either delete theaudience.defaultline and reload, or runpmk gateway audience default pmto flip it explicitly.
Tests
@pmk/cli 362 → 362 (unchanged). One existing back-fill assertion in gateway.test.ts flipped from default === "tech" to default === "pm" to match the new factory; the assertion's intent was always "legacy configs get back-filled with the default" — the specific value is what changed. All 412 workspace tests still pass.
Operator note
If you've been running v0.13.0 (or any earlier version) and want non-IT users to get the new behaviour, run on the gateway host:
pmk gateway audience default pm
pmk gateway audience set <YOUR-IT-USER-ID> tech
# Then restart the gateway so the new config snapshot is in-memory:
# Ctrl+C the foreground process, or kill -TERM <pid>, then `pmk gateway start`.
The audience field is loaded into memory at adapter construction (the same in-memory snapshot caveat as escalation), so a config change requires a restart to bite.
[v0.13.0] — 2026-05-20 — SlackGateway harness + adapter decomposition + FIFO inflight queue
Why
The v0.7 → v0.12 gateway picked up presence broadcast, per-channel audience, monthly audit logs, msg_too_long hardening, anthropic-api as default, token.usage rollups — all landed on a 1708-line SlackAdapter monolith with zero automated coverage. Every change in that series had to be verified by hand via live-Slack dogfood because instantiating the adapter required real Slack tokens and a live socket connection. Two problems compounded:
- No safety net for a refactor. The adapter mixed Slack transport + session + LLM + mra-ask + escalation + atoms + reactions + presence + slash commands. Splitting it into focused modules — the only way to keep adding features without the file becoming write-only — was hostage to live verification per change.
- Two latent multi-user bugs were invisible. Rapid follow-up messages from the same user were silently dropped behind a misleading
:hourglass: 你上一則訊息還在處理,請稍候notice that implied queue semantics; and parallel @-mentions in the same channel raced on a sharedchat-session.json, so the second writer's turn was overwritten on disk even when both Slack-side replies landed.
v0.13 closes both: a constructor-injected fake transport + 27-test integration harness lands first, then four tranches incrementally extract focused modules under that safety net, and the two multi-user bugs get fixed with semantics that match what the UX always implied.
Added
SlackGatewayintegration harness (PRs #52) —packages/cli/test/harness/slack-fakes.tsprovidesFakeWebClient,FakeSocketModeClient,FakeLlmProvider,FakeMra, andbuildHarness().SlackAdapterconstructor now accepts optionalweb/socket/llm/mraDoctor/runMraAskoverrides (production path is byte-equivalent — defaults to real wiring when none supplied). 27 new integration tests cover DM happy-path, mra-ask escalate, channel @-mention free-chat, channel-with-active-case,/pmk adminslash command, reaction-based atom approval, presence broadcast / restart matrix, msg_too_long retry, and envelope dedup across all three event paths. The harness is the prerequisite that made tranches 1–3 below safe to land.- Multi-user channel concurrency (PR #53) — the
inFlightlock key changes fromchannelIdto${channelId}:${userId}. Different users in the same channel can now ask the bot questions in parallel without waiting for each other's 60–90 s mra-ask round. A single user's rapid double-tap stays serialised (the original intent of the lock). Busy-notice text changes from:hourglass: 已有訊息在處理中→:hourglass: 你上一則訊息還在處理,請稍候to match the now per-user semantics. - Append-only channel message log (PR #53) —
packages/cli/src/gateway/channel-log.tsreplaces the read-modify-writechat-session.jsonwith per-channel JSONL:~/.pmk/gateway/slack/channels/<channelId>/messages.jsonl(and per-thread underthreads/<ts>/messages.jsonl).appendChannelTurnsuses a singlefs.appendFileSyncsyscall → POSIX-atomic, so concurrent parallel writers from the new per-user-per-channel lock all land instead of last-write-wins dropping turns.loadChannelTurnsstreams + skips malformed lines + supportslimit/sinceMsfilters, and triggers a one-time migration from legacychat-session.jsonon first read. - FIFO inflight queue (PR #55) —
packages/cli/src/gateway/slack/inflight-queue.tsreplaces the pre-v0.13 "drop second message + misleading busy notice" model. Rapid follow-up messages behind an in-flight LLM round now actually queue FIFO (default depth 3) and drain in submission order. Key is the caller's:userIdfor DM,${channelId}:${userId}for channel @-mention. At-cap submissions get an explicit:no_entry: 你已有多則訊息排隊中(上限 3 則),請等回覆後再發rejection notice instead of silent drop. Queued submissions get:hourglass: 你上一則還在處理,這則已排入隊伍(會依序處理). Slack envelope handlers now return fast (fire-and-forget) so the 3-second ack window stays comfortable. - Graceful shutdown drain (PR #55 review) —
SlackAdapter.stop({ drainTimeoutMs })reordered tosocket.disconnect→queue.waitForAll(bounded) →presence.offline. The SIGINT/SIGTERM handler passes25_000ms (K8s SIGKILL kicks in at 30 s); a stuck LLM round logsdrain timed out after 25000ms; abandoning remaining in-flight workand continues so SIGKILL isn't what reaps the process. Without the drain, queued turns died withprocess.exit(0)— users who saw the "排入隊伍" notice never got a reply.
Changed
SlackAdapterdecomposed (PRs #52, #54, #55) — three tranches under the new harness:- Tranche 1 extracts
slack/presence.ts(146 lines — broadcast fan-out + #44 suppress-on-fast-restart),slack/envelope-dedup.ts(45 lines — bounded-LRU dedup),slack/concurrency.ts(36 lines —runWithConcurrency). Adapter1708 → 1597(-111). - Tranche 2 extracts
slack/escalation.ts(308 lines — outbound@-mention IT contacts+ inbound IT-reply absorb + asker-synthesis follow-up). Adapter1640 → 1411(-229). - Tranche 3 extracts
slack/free-chat-turn.ts(509 lines — full turn orchestration: seed + retrieval + prune + LLM + mra-ask + escalate + reply) andslack/inflight-queue.ts(141 lines — the new FIFO queue). Adapter1411 → 1010(-401). - Net: -698 lines (-41%) from the v0.12.0 baseline. Tranche 4 (dispatcher cleanup, target
<800lines) is deferred.
- Tranche 1 extracts
- Behaviour byte-equivalent across all three tranches; the harness's tests pass without modification at every step, which is exactly the safety net tranche 1 promised.
Tests
@pmk/cli 312 → 362 (+50). Major additions:
- Integration harness (27 tests, Phase 1–3) — DM happy-path (3), mra-ask escalate round (3), channel @-mention free-chat + case path (2),
/pmk adminslash (3), reaction-based atom approval (4), presence broadcast restart matrix (5), msg_too_long retry hardening (3), envelope dedup across DM / @-mention / slash (4). - Multi-user concurrency (PR #53, 2 tests) — different users in same channel proceed in parallel; same user double-tap blocked with per-user notice.
- Append-only channel log (PR #53, 7 tests) — append-on-empty, FIFO ordering, malformed-line skip,
sinceMscutoff, legacy migration round-trip,entriesToMessagesshape, multi-channel isolation. - FIFO inflight queue (PR #55, 10 unit tests) —
enqueuecontract (ran/queued/QueueFullError), defaultmaxDepth=3, FIFO order under release, key independence, error isolation (throwing work doesn't poison queue),waitForAllcorrectness,onLog-throwing-defence (broken logger can't lock a key out forever). - Inflight queue adapter integration (PR #55, rewritten suite) — different users still parallel without queue notice, same-user double-tap queues with correct UX text, 4th submission rejected with cap notice.
- Graceful shutdown drain (PR #55 review, 2 tests) —
stop()waits for in-flight work before broadcasting offline;stop({ drainTimeoutMs })honours timeout when work is genuinely stuck and logs the abandon.
Total across the workspace: 362 → 412 pass, 0 fail.
Operator note
User-visible Slack text changed. Rapid follow-up DM/@-mentions now see one of three messages instead of the pre-v0.13 single dropped one:
- 1st follow-up while bot is replying:
:hourglass: 你上一則還在處理,這則已排入隊伍(會依序處理)— and the message will be processed, not dropped. - 4th submission while 3 are already queued:
:no_entry: 你已有多則訊息排隊中(上限 3 則),請等回覆後再發— this one IS dropped, but explicitly. - Channel @-mention from a different user while bot is busy with someone else: no notice at all, both run in parallel.
Zero migration on disk. The new channels/<channelId>/messages.jsonl layout migrates from legacy chat-session.json automatically on first read of an existing channel. Operators can rm legacy files manually after observing the migration in events.log, but the reader is idempotent — re-running upgrade is safe.
Shutdown takes longer. Pre-v0.13 graceful shutdown was sub-second; v0.13 now waits up to 25 s for queued / running LLM rounds to complete before exiting. The trade-off: users who saw the "排入隊伍" notice actually get their reply, instead of silent drop on kill. K8s terminationGracePeriodSeconds should be ≥30 (the default).
[v0.12.1] — 2026-05-19 — README + CLI version catch-up
Why
Two non-feature drifts caught during a v0.12 review, neither blocking but both visible: the README Latest release section was stuck at v0.10.1 while the tag stream had marched to v0.12.0 (two minor versions of gateway hardening invisible to anyone reading the repo front page), and pmk --version had been reporting 0.7.0-dev since v0.7.0 because Commander's .version() was a string literal that the v0.10.1 bump-version.mjs work never reached. Tying both off in one patch so the v0.12 series ends with the repo's outward-facing version surfaces in sync.
Fixed
pmk --versionno longer hardcoded.packages/cli/src/index.tsnow importsversionfrom../package.json(viaresolveJsonModule) and feeds it into Commander's.version(). The path resolves identically at compile time (src/../package.json) and at runtime from the builtdist/index.js(dist/../package.json), both pointing atpackages/cli/package.json, sonpm run version:bumppropagates to the CLI surface from this release forward with no additional wiring.
Docs
- README
Latest releasecaught up v0.10.1 → v0.12.0. Header date + version updated, narrative paragraph extended with the v0.10.1 → v0.12.1 chain (gateway presence + per-channel audience + monthly audit logs in v0.11.0,msg_too_longthree-layer defense +Context safetyaudit section in v0.11.1,anthropic-apisoft-flip +token.usage+Token usageaudit section in v0.12.0, this README/CLI catch-up in v0.12.1), and four new rows in the release table.
Tests
@pmk/cli 312 → 312 (unchanged): the version-source change is a 1-line wiring fix verified by a smoke test (pmk --version reports the bumped value, 0.12.0 before and 0.12.1 after version:bump). No new test was added because the fix relies on TypeScript's resolveJsonModule + Node's relative-require resolution, both of which are exercised by every release build; a dedicated unit test would test the runtime, not the change.
Operator note
Zero migration. Existing ~/.pmk/ state and gateway config carry forward unchanged. Anyone scripting against pmk --version should be aware the reported value finally tracks the real release tag — if a script relied on the stale 0.7.0-dev marker, update it.
[v0.12.0] — 2026-05-08 — gateway: anthropic-api as default provider
Why
v0.11.1 hardened the gateway against msg_too_long by lowering caps and adding an auto-retry path, but cause #2 from the 2026-05-07 incident — claude-agent-sdk spawning the local claude CLI and inheriting the host's ~/.claude/ config (skills/hooks/MCP descriptions) as un-budgeted system context — was absorbed by tighter caps, not eliminated. v0.12.0 flips the default to the direct Anthropic SDK so SDK overhead is no longer a budget unknown, and restores cap headroom.
Spec: apps/docs/docs/plans/2026-05-08-gateway-anthropic-api-default.md. Migration: v0.12 migration notes.
Changed
- Default LLM provider auto-resolves to
anthropic-apifirst (wasclaude-agent). Soft flip — users withANTHROPIC_API_KEYset auto-switch; users without it stay onclaude-agentwith no behavioural change.PMK_PROVIDER=claude-agentstill pins the legacy path explicitly. - Cap defaults restored to operationally useful values now that SDK overhead is gone on the default path:
PMK_MAX_SESSION_TOKENS25_000 → 60_000PMK_SEED_CAP12_000 → 30_000PMK_MRA_RESULT_CAP16_000 → 40_000
gateway initprompts forANTHROPIC_API_KEYafter Slack tokens; stored in~/.pmk/gateway.jsonapiKeyfield at mode 0600. Empty input keeps existing value or falls back to env var. The running gateway daemon needs a graceful restart to pick up a newly-set apiKey (matches the existing audience/escalation config-mutation pattern).
Added
token.usageevent inevents-YYYY-MM.log— emitted byAnthropicApiKeyProvider.chat()after each successful stream completion, when anactoris provided inChatOptions. Fields:actor,provider,model,inputTokens,outputTokens, optionalcacheReadTokens/cacheCreationTokens. Best-effort write — failures don't break the chat.Token usagesection inpmk gateway auditrolls up the new events: total in/out, cache read (when non-zero), top-3 per-actor by input tokens, per-model breakdown.ChatOptions.actoroptional field on theLlmProvider.chat()interface for usage attribution. Threaded throughchatWithContextRetryautomatically; CLI command-side wiring is future work.
Tests
@pmk/cli 304 → 312 (+8): resolver.ts autoResolve order (apiKey-preferred + fail path), AnthropicApiKeyProvider.chat() token-usage emission with mocked stream + finalMessage(), no-emission when actor undefined, events.ts round-trip for token.usage, audit.ts aggregation, audit-format.ts Token usage rendering for non-zero + zero cases. Cap-default test assertions flipped from v0.11.1 values to v0.12.0 values.
Forward-looking
claude-agent provider stays as a soft-flip fallback indefinitely. Re-evaluate deprecation in v0.13+ based on usage data from the new Token usage audit section. $-cost calculation is a v0.13+ candidate, gated on a stable price-table source. SlackGateway integration harness remains tracked as a v0.11.2 follow-up.
[v0.11.1] — 2026-05-07 — gateway msg_too_long hardening
Why
A live Slack thread on 2026-05-07 returned pmk 內部錯誤:An API error occurred: msg_too_long after several mra-ask rounds. Root-cause analysis surfaced four issues and v0.11.1 layers defenses against all of them so the failure mode does not reach production users again. See apps/docs/docs/plans/2026-05-07-gateway-msg-too-long-hardening.md for the full design spec, and 2026-05-07-gateway-msg-too-long-hardening-implementation.md for the per-task TDD plan.
Fixed
msg_too_longno longer reaches end users. Three layered defenses:- (a)
pruneSessionIfNeedednow runs before the LLM call (was after — closed a fail-loop introduced in v0.8.1 where a session over budget could never recover because prune only fired after a successful call). - (b) The PKB seed and
mra-askresults are capped at write-time so a single bloated message cannot single-handedly exhaust the input window. - (c) Any residual
msg_too_longtriggers a typedPmkContextTooLongError, an automaticforcePruneToMinimum, and a retry. The reply is prefixed with:scissors: 對話過長,已自動裁掉 N 輪舊訊息so users know context was trimmed. Hard failure (both calls reject) shows:x: 對話太長,請開新 thread 重新提問instead of the raw API error.
- (a)
Changed
PMK_MAX_SESSION_TOKENSdefault lowered 60_000 → 25_000 to leave headroom for system prompt, retrieval prefix, the SDK-inherited host context (claude-agent-sdkspawns the localclaudeCLI, which inherits~/.claude/skills/hooks/MCP descriptions), the new turn, and the model's reply.
Added
- New env vars
PMK_SEED_CAP(default 12_000 chars) andPMK_MRA_RESULT_CAP(default 16_000 chars) for per-host tuning. The previously-hardcoded 24_000-charmra-asktruncation inbuildMraSuccessMessageis replaced byPMK_MRA_RESULT_CAP. - New event types in
events-YYYY-MM.log:context.exceeded(withphase: "first-call" | "synthesise"),context.force-pruned,message.capped(withkind: "seed" | "mra-result"). pmk gateway auditgains aContext safetysection rolling up the new events. Tighten the*_CAPenv vars ifcontext.exceededappears in your weekly audit.- Helper
chatWithContextRetryextracted topackages/cli/src/gateway/slack/context-retry.tsso the retry+force-prune+events pattern is unit-testable in isolation (noSlackGatewayintegration harness needed) and reused at both LLM call sites (runFreeChatTurnfirst-call,synthesiseAfterMramra-ask round).
Tests
@pmk/cli 274 → 304 (+30): unit coverage for capMessageContent, forcePruneToMinimum, pruneSessionIfNeeded extras-aware budgeting, approxTokensFor with extra param, PmkContextTooLongError detection, the six-discriminant chatWithContextRetry (happy / non-context error / context-then-success-with-scissors / context-then-fail / dropped=0 degenerate / phase=synthesise audit), audit contextSafety rollup, formatter Context safety section non-zero + zero-count rendering, and the three new event-type round-trip in gateway-events.test.ts.
The seed-cap and mra-result-cap wiring sites in slack/index.ts and the runFreeChatTurn retry-prefix wiring rely on the constituent helpers' unit tests + manual verification (no SlackGateway integration harness in this release; tracked as a follow-up).
Forward-looking
v0.12 is planned to switch the gateway provider from claude-agent-sdk to anthropic-api, removing the SDK-inherited host-context as a budget unknown. The cap mechanism from v0.11.1 stays; only the budgets relax toward the model's true context window. See the v0.12 stub at the end of the v0.11.1 design spec.
[v0.11.0] — 2026-05-05 — gateway presence + per-channel audience + monthly audit logs
GitHub release · closes #23, #44 · milestone v0.11
Why
Two issue-driven items plus one v0.10.x debt cleanup, sized to ship as one minor release:
- #44 — kill→restart cycles broadcast spurious "重新上線" (live-observed during v0.10.0 verification).
- #23 —
pickAudiencehad no channel tier, forcing per-user overrides for "this channel defaults to exec" cases. - events.log unbounded growth TODO from v0.10 — bumped in priority because #44 adds presence events on every start/stop.
See the v0.11 migration notes for a focused operator-facing summary of the layout + behaviour changes.
Added
- Per-channel audience override (#23) — new
cfg.audience.channels: Record<channelId, AudienceKey>tier between per-user and workspace default. CLI:pmk gateway audience set-channel <channelId> <key>/unset-channel. Slack admin:/pmk admin audience set-channel #channel <key>/unset-channel.extractChannelIdhelper handles<#C0X|name>mention,<#C0X>bare mention, and rawC0X/G0X/D0XIDs. Resolution order at turn time: per-user → per-channel → workspace default. - Graceful-shutdown marker (#44) — single-use file at
~/.pmk/gateway/shutdown-markerwritten onSIGTERM/SIGINT. The nextstartHeartbeat()reads + consumes it to distinguish "kill -> restart" from a real crash;wasOffline=falseand the back-online broadcast is suppressed when the offline gap is under 5 minutes. - Presence event types in
events.log(#44) —gateway.onlineandgateway.offlinejoin the JSONL stream with monotonic per-processseq, human-readablereason(crash-recovery/graceful-fast-restart/graceful-long-downtime/shutdown),broadcastbool, andofflineDurationMs. Lets the audit detect rapid restart cycles and the graceful-vs-crash split. - Monthly-partitioned JSONL ledger (PR #47) — new
packages/cli/src/gateway/monthly-jsonl.tsshared util powers bothevents.logandadmin.log. Files are now~/.pmk/gateway/events-YYYY-MM.log/admin-YYYY-MM.log(UTC month). Legacy single-file ledgers from v0.10 are still read-only-merged so upgrades don't lose history. No eviction — operators canrmancient partitions manually; the reader silently skips missing months.
Fixed
- Restart-cycle broadcast spam (#44) — heartbeat is no longer deleted on graceful shutdown (it stays for
offlineDurationMsaccounting), andbroadcastBackOnline()checks the gap before posting. The same change exposes the issue's secondary symptom:broadcast()'s O(N) serial fan-out is nowrunWithConcurrency(limit=3), finishing in seconds instead of 20+ s and isolating per-recipient errors. Live-Slack verified: a 1.3-second graceful restart recordsgateway.online ... broadcast:false offlineDurationMs:1332and the channel sees no spurious "重新上線" message. events.logunbounded growth (v0.10.x debt) — closed by the monthly partitioning above.
Tests
248 → 274 (+26 across @pmk/cli). Major additions:
- Heartbeat marker decision matrix (5 branches: first boot, marker fresh, marker stale, no-marker fresh heartbeat, no-marker stale heartbeat) + corrupt-marker safety + upgrade-migration story
runWithConcurrency(4 cases: empty list, peak in-flight respected, single-task rejection isolated, limit > task count)pickAudiencechannel tier (5 cases: channel applies absent user, per-user beats channel, fall-through, undefined channelId, back-fill on old config) + empty-string channelId guard/pmk admin audience set-channel/unset-channelend-to-end (mention wrapping, raw ID, garbage rejection, round-trip)- Monthly partitioning (current-month write + legacy NOT written, legacy + partition merge order, multi-month aggregation with
sinceMscutoff, default 12-month window, legacy mixed-content malformed-line skip, admin-log mirror)
Plus a new @pmk/shared test surface (was 0 → 24): shape-based snapshot tests covering BASE_RULES, all four audience prompts, pickGatewayPrompt round-trip, AUDIENCE_KEYS, PROMPTS map coverage, and DEFAULT_CONFIG shape.
Total across the workspace: 274 → 324 pass, 0 fail.
Operator note
Zero migration. All schema changes are additive and back-fill-compatible. First kill→restart after upgrade still broadcasts "重新上線" once — the v0.10 gateway shut down without writing a marker, so the v0.11 build correctly treats it as a fresh boot. From the second graceful restart onward, suppression works.
For tail-style debugging, switch from tail -f ~/.pmk/gateway/events.log to tail -f ~/.pmk/gateway/events-$(date -u +%Y-%m).log (note the -u for UTC, since partitions roll on UTC month boundaries).
[v0.10.1] — 2026-05-05 — workspace version sync + mra stdout cap
Why
Two trailing items from the v0.10 milestone close, neither feature-shaped: workspace package.json files had drifted to 0.3.0 while git tags marched to v0.10.0, and runMraAsk accumulated stdout via += with no upper bound — both observational risk on v0.10.0 day, but worth tying off before the v0.11 milestone opens its own surface.
Added
scripts/bump-version.mjs+ root npm scriptversion:bump— bumps root + everyapps/*andpackages/*package.jsonto a given semver in one pass. Used to bring all 7 manifests in sync to0.10.1. Lands the tag-vs-manifest sync into the release flow so the next minor close cannot drift again.- Exported
MAX_MRA_STDOUT_BYTES(10 MiB) frompackages/cli/src/adapters/mra.ts— soft cap on capturedmra askstdout, matching the oldexecFilemaxBufferdefault.
Fixed
runMraAskstdout accumulator — switched fromstring +=tochunks.push() + jointo remove the latent O(n²) string-concat cost on large outputs, and added a soft 10 MiB cap that SIGTERMs the child if exceeded. Defence in depth: livemra askrounds are KB-scale, but a wedged subprocess streaming unbounded output would have pressured host memory in the prior implementation. The overflow reason is also classified as non-transient, so the v0.7.3 retry-once policy doesn't burn a second round on a path that just reproduces the same overflow.package.json#versionworkspace drift — root and 6 sub-packages now report0.10.1instead of the stale0.3.0they had carried since v0.4.
Tests
247 → 248 (+1): runMraAskWithBinary overflow case — fake mra writes past the cap, asserts ok=false, reason mentions both stdout exceeded and the exact MAX_MRA_STDOUT_BYTES byte count, and attempts === 1 (proves overflow is treated as non-transient).
Total across the workspace: 273 → 274 pass, 0 fail.
Operator note
Zero migration. The cap is generous (10 MiB) and the overflow reason surfaces clearly in events.log (mra-ask.end ok=false) plus the user-facing failure message. Hosts that previously relied on capturing >10 MiB of mra ask stdout (none observed in dogfood) would now see a non-ok result with the explicit cap — but at that scale the prior code path was already O(n²) and would have stalled the gateway.
For the next release, run npm run version:bump <semver> before tagging — the bump should be its own commit so the tag points at a tree where every manifest already reads the new version.
[v0.10.0] — 2026-05-04 — gateway observability + Slack UX
GitHub release · closes #22, #24 · milestone v0.10
Added
pmk gateway audit [--days N](#24) — operator-facing rollup of recent knowledge-loop activity: per-user / per-audience turn breakdown, mra-ask success/retry/fail split with median duration, escalate triggered / absorbed / pending counts and median time-to-IT-reply, atom corpus stats with top contributors, plus flags for stuck pending atoms (> 24h) and stale escalations (> 48h). Window defaults to 7 days;--daysaccepts 1–365.~/.pmk/gateway/events.log— append-only JSONL ledger for the four event types the audit consumes (turn.processed,mra-ask.end,escalate.triggered,escalate.absorbed). Mirrorsadmin.login shape and contracts; tolerant reader skips malformed lines.- Live mra-ask progress in Slack (#22) —
runMraAsknow usesspawninstead ofexecFileso each stdout line streams into the placeholder message via a 3-second last-line-wins throttle. The 30–90s mra round shows[ask] PKB loaded,[ask] querying...etc. tick by instead of a static spinner.web.chat.updaterate well under Slack Tier 3; trailing fire cancelled on completion so a late progress line can't briefly overwrite the synthesised reply.
Fixed
- ANSI escape codes in progress placeholder — live-Slack verification on 2026-05-04 caught a defect:
mracolorizes its[ask]/[pkb]tags with ANSI SGR sequences (\x1b[1;37m[ask]\x1b[0m querying: erp), and the original sanitizer in #43 only stripped Slack mrkdwn meta. Slack rendered the residual[1;37m/[0mas literal text, making the streaming UX worse than the static spinner v0.10 was meant to replace. Sanitizer now strips ANSI SGR before mrkdwn meta. Extracted assanitizeProgressLineinsrc/gateway/slack/progress.tsfor direct unit testing. mraDoctorstale-workspace fall-back — long-standing comment-vs-code mismatch insrc/adapters/mra.ts. Comment promised "stalecfg.mraWorkspacefalls back to cwd walk so a host with a valid workspace ancestor isn't silently broken"; code returnedok:falseinstead. Code now matches the spec, with the error reason mentioning both the stale config and the failed walk so operators see the full picture.
Notes
- Audience binding is captured at turn time, so changing
audience defaultafter the fact does not rewrite the audit's history. - Atom corpus stats (
total,approved,pending,topContributors) are intentionally lifetime, not window-scoped — atoms persist in~/.pmk/knowledge/across windows.
Tests
193 → 247 (+54 across the milestone): pmk gateway audit formatter + integration cases (#24), throttle leading/trailing/cancel behaviour (#22), spawn-based runMraAsk retry / SIGTERM / progress / partial-line handling (#22), sanitizeProgressLine ANSI + mrkdwn + length-cap, mraDoctor fall-back semantics.
Operator note
Zero migration. events.log auto-creates on first write; progress streaming activates automatically when an mra-ask round runs. If cfg.mraWorkspace was previously set to a now-deleted path, the runtime now silently falls back to a cwd-walk (the gateway startup pre-flight still warns at boot, so misconfiguration isn't hidden — just no longer fatal at request time).
[v0.9.1] — 2026-04-28 — /pmk real Slack slash-command (no leading-space workaround)
GitHub release · closes #39
Why
Real-Slack verification of v0.9.0 found that typing /pmk admin help in Slack triggered Slackbot's "/pmk 是無效指令" intercept and never reached the bot. Slack's client blocks /-prefixed messages whose slash-command isn't registered on the app side. The only way to actually deliver the message was to type a leading space ( /pmk admin help) so the gateway's existing message-event path could pick it up after text.trim(). Same gap had existed for every /pmk command since v0.7.0 (help, open, show, close, cases).
Fixed
- Real Slack slash-command —
/pmkis now registered as a Slack slash-command on the app side; SlackAdapter subscribes to the Socket Modeslash_commandsenvelope (packages/cli/src/gateway/slack/index.ts). No more leading-space workaround. Slack autocompletes/pmkand the bot replies as a top-level message (slash commands have no anchoring message, so no thread). - The legacy
/pmk ...text-message path stays in place as a fallback for users who learned the workaround and for deployments where the slash-command isn't registered. - Empty body (
/pmkalone) routes tohelpso first-time users discover the surface.
Added
slashCommandArgsFromBody(body)— exported pure helper that translates a Slackslash_commandsenvelope body intohandleSlashCommandargs. Lets us unit-test the rest/scope decision without instantiatingSlackAdapter.
Changed
handleSlashCommand'sthreadTsis now optional. Slash-command envelopes have no anchor message, so omitting it is correct; the legacy text-message path still passes a thread_ts.chat.postMessageonly includesthread_tswhen defined (was always passing it before, even when undefined).
Tests
185 → 193 (+8): slashCommandArgsFromBody for DM/channel scope split, empty-text fallback to help, missing user_id/channel_id returns null, undefined body returns null, DM-only check stays downstream.
Operator note
Existing v0.9.0 deployments need to (one-time):
- Register
/pmkas a Slash Command athttps://api.slack.com/apps/<APP_ID>/slash-commands(Socket Mode is on, no Request URL needed) - Reinstall the app to add the
commandsscope - Restart
pmk gateway startwith the v0.9.1 binary
Until step 3 is done, the leading-space path is the only one that works. After step 3, both paths work in parallel.
[v0.9.0] — 2026-04-28 — Slack admin commands + audit log
GitHub release · closes #31
Added
/pmk admin <subcommand>runs gateway-config mutations from inside Slack — no host terminal needed for day-to-day ops. Subcommands coverstatus,audience,escalation,atoms(list/show/approve/reject),admins, andaudit. See the Admin commands section of the lifecycle doc.pmk gateway admin <add|remove|list|audit>— host CLI counterpart for bootstrapping the very first admin and rotating the set. Bootstrap is intentionally terminal-only — there is no Slack path to grant yourself admin.- Append-only audit log at
~/.pmk/gateway/admin.log. Every admin mutation, whether from Slack or CLI, writes one JSONL line capturingactor,origin,action,args,ok, and (on failure) areason. Surfaced via/pmk admin audit [N]andpmk gateway admin audit [N]. cfg.admins: string[]in~/.pmk/gateway.json. Back-fills to[]for legacy configs so existing deployments keep working with no migration step.isAdmin(cfg, userId)helper used by the Slack adapter's/pmk adminroute gate.
Trust model
- Bootstrap requires terminal access. The first admin must come from the host CLI; you cannot grant yourself admin from Slack.
- DM-only.
/pmk adminin a channel returns:no_entry_sign:and does nothing. Keeps audit-relevant mutations out of channel scrollback. - Last-admin protection. Removing the only admin is refused — even self-removal — to prevent locking the workspace out of the Slack admin path entirely. Add a replacement first.
- Slash-command surface is a deliberate subset.
init, token rotation,atoms edit, process stop/restart, and blocklist mutation are all CLI-only.atoms editin particular: pasted Slack content would land verbatim in retrieval, and the CLI's$EDITOR-with-validation path is safer.
Tests
166 → 185 (+19): isAdmin true/false + legacy back-fill, audit-log round-trip + tail-limit + malformed-line skip + non-fatal write failure, Slack handler help / unknown subcommand / audience set + invalid tier / admins add+remove + last-admin protection + invalid Slack-id rejection / audit subcommand surfaces entries / escalation default vs repo pool isolation, plus mention parsing (<@U0X>, <@U0X|name>, bare U0X, garbage).
[v0.8.5] — 2026-04-28 — Slack reaction-based atom approval
GitHub release · closes #21
Added
- ✅ / ❌ reactions on the bot's pending-notice now approve / reject the atom in-flow:
- ✅ (
white_check_mark,heavy_check_mark,+1) →approveAtom, posts "📚 已生效..." reply - ❌ (
x,-1) →rejectAtom(deletes the file), posts "🗑 已捨棄..." reply
- ✅ (
- Trust model: only the original IT contributor (
atom.source.contributorUserId) can react. Other reactors are silently ignored. Random thread participants can't approve atoms. KnowledgeAtom.approval?: { channelId, messageTs }captures the bot's confirmation posttsso reactions can be mapped back to the originating atom. Atoms saved before v0.8.5 don't have this anchor and can't be reaction-approved (CLI fallback still works).findAtomByApprovalMessage(channelId, messageTs)helper for the Slack handler.
Changed
gateway initwalkthrough now listsreactions:readscope andreaction_addedevent subscription as v0.8.5+ requirements. Existing v0.7.x apps without these scopes keep working — no events fire, TTL auto-promote remains the safety net.- The pending-notice text now invites reaction directly: "直接 ✅ 或 ❌ react 這條訊息可立即 approve / reject".
Tests
162 → 166 (+4: anchor lookup matches; mismatched channel/ts returns undefined; legacy atoms without anchor return undefined; approval round-trips through save/load).
[v0.8.4] — 2026-04-28 — BM25 / TF-IDF retrieval for knowledge atoms
GitHub release · closes #19
Added
- New
packages/cli/src/gateway/atom-index.ts— BM25-scored TF-IDF index over approved atoms via@pmk/rag. Pending atoms are excluded at index-build time (the v0.7.4 TTL gate is preserved). Index file persisted at~/.pmk/knowledge/.index/<scope>.json; auto-rebuilds when any atom file's mtime is newer than the index'sbuiltAt. pmk gateway atoms reindex [--scope <name>]— force-rebuild the index. Useful after tweaking thresholds or to confirm the index is current.PMK_ATOM_VECTOR_THRESHOLDenv var — corpus-size threshold above whichsearchAtomsswitches from keyword overlap to BM25. Default50.
Changed
searchAtomsnow picks its scoring path at runtime by corpus size:< threshold(small corpus): keyword + tag overlap (the v0.7.0 path; cheap, predictable)>= threshold(large corpus): BM25 via the new index
- BM25 falls back to keyword on empty results, so single-token CJK queries that the tokenizer can't handle still work.
Why
The keyword + tag scoring drifts as the corpus grows past a few dozen atoms — partial token matches and CJK bigram noise surface irrelevant atoms above relevant ones. Atom retrieval that's worse than no retrieval is dangerous because the model treats them as ground truth. BM25 fixes the ranking quality without requiring an external embedding API.
(Issue title was "vector retrieval" but @pmk/rag is BM25 / TF-IDF, which is the practically-useful upgrade. Pure JS, no embedding cost, no network dependency.)
Tests
158 → 162 (+4: index excludes pending, BM25 returns approved-ordered, approvedAtomCount filter, mtime invalidation triggers rebuild).
[v0.8.3] — 2026-04-28 — atoms search + edit CLI + commander option pass-through
GitHub release · closes #20
Added
pmk gateway atoms search <query> [--scope <name>] [--limit N]— wrapssearchAtoms()for dry-run retrieval ranking. Useful for sanity-checking after a new atom lands ("would this be retrieved when someone asks X?") without DM-ing the bot. Output: rank | id-prefix | scope | tags | question table.pmk gateway atoms edit <id-or-prefix>— opens the atom's.mdin$EDITOR(fallbackvi). Post-save validation: re-parses viagray-matter, ensuresidandcreatedAtare unchanged, restores the pre-edit version on parse failure. Tag/summary/answer changes are unrestricted.
Fixed
- Commander option pass-through. Previously
pmk gateway atoms list --pending,--scope,--limitetc. were eaten by Commander as unknown root options before reaching the gateway handler. Workaround waspmk gateway atoms list -- --pending. Now--xxxflags pass through cleanly viaenablePositionalOptions()+passThroughOptions().- The deprecated
pmk gateway escalation add --default <userId>form still works (still emits a deprecation warning).
- The deprecated
Tests
158/158 pass (no new — the underlying searchAtoms and findAtomByPrefix are tested; CLI integration verified via manual smoke).
[v0.8.2] — 2026-04-28 — escalate self-tag detection
GitHub release · closes #30
Fixed
- When a model emits
escalatebut the resolved escalation pool is empty (or contains only the asker themselves), the gateway no longer silently logs and drops the mention. It now posts a visible:warning:message in the Slack thread naming the config gap and the exactpmk gateway escalation add ...commands to fix it. The pending-escalation marker is also skipped (no point waiting for an absorb that can't happen). - The asker is filtered out of the resolved pool before any
@-mention. Previously, if the only configured contact happened to be the same person who asked the question, the bot would@-mention them at themselves.
Added
pickEffectiveEscalationPool(cfg, repo, askerUserId)helper ingateway/config.ts— single-source-of-truth for "which contacts should we @-mention given this asker?". Used byhandleEscalationand unit-tested in isolation.
Caught by
2026-04-28 dogfood: real escalate flow on a PM scoping question logged escalate requested but no contacts configured; skipping mention while the bot's Slack reply degraded to prose ("建議兩個行動: SQL 查 / 找 AOE/PM 同仁"). The host had no way to tell from Slack that the v0.7 escalate flow was suppressed for a config reason.
Tests
156 → 158 (+2: pool-with-asker filters self; both-pools-empty stays empty).
[v0.8.1] — 2026-04-28 — session context-window auto-pruning
GitHub release · closes #18
Added
pruneSessionIfNeeded(session)ingateway/messaging.ts— when a session crossesMAX_SESSION_TOKENS(default60_000, override viaPMK_MAX_SESSION_TOKENSenv), drops the oldest non-seed turns. Always preserves the PKB seed pair plus the most recentKEEP_RECENT_TURNS(default 10) user/assistant pairs; inserts a synthetic(此處省略 N 輪較舊的對話以節省 context)marker so the model knows there was earlier history.- Idempotent — re-running on an already-pruned session is a no-op until enough new turns push back over cap.
- Host log line
pruned session: dropped N turn-pair(s); now <tokens> approx tokensconfirms when it fires.
Why
Until v0.8.1, UserSession.messages accumulated forever. Each gateway-DM turn pushes 2 messages (user + assistant), the mra-ask round adds 2 more, the PKB seed adds 2 on first turn. After ~50 turns in a single thread the session approaches the model's context window — slow LLM round-trips, eventual context_length_exceeded, linear token-cost growth. v0.8.1 caps that.
Tests
151 → 156 (+5: under-cap no-op, over-cap pruning preserves seed + tail, idempotent on already-pruned, no-seed branch, single-huge-message edge case).
[v0.8.0] — 2026-04-28 — pm audience tier
GitHub release · closes #27
Added
- New audience tier
pmbetweentechandbiz. Keeps full structural depth (file paths, model names, real findings) for what exists, but translates questions back to the user into PM vocabulary — no formulas, no SQL, no bare schema column names. Includes a translation cheat-sheet in the prompt so the model has explicit examples ("vCPM = cv / impression × 1000 × price?" → "vCPM 在你們有兩種意思:對廣告主報的成本 vs 對媒體分潤的單價。要看哪一種?"). pmk gateway audience set <userId> pmandpmk gateway audience default pmnow valid.AUDIENCE_KEYSexported from@pmk/sharedupdated to["tech", "pm", "biz", "exec"].
Caught by
Live dogfood 2026-04-28: a real PM project-scoping question got an excellent tech-tier reply (BigQuery vs API Gateway structural finding was perfect) but alignment questions phrased in formula-grade vocabulary that no PM could answer without first re-asking engineering — defeating the point. The PM tier closes that gap.
Tests
148 → 151 (+3: prompt body assertions, AUDIENCE_KEYS shape, per-user pm setting).
[v0.7.5] — 2026-04-28 — mra timeout-kill mis-classification
GitHub release · PR #25
Fixed
- Critical: Node's
execFiletimeout-kill produceserr.killed=true/err.signal="SIGTERM"(witherr.code=null), but the v0.7.0 detection checkederr.code === "ETIMEDOUT"— so timeouts had never been correctly identified. Every timeout was labeledCommand failed: <argv>, mis-leading operators and the LLM, and tripping the v0.7.3 retry-once on questions that always needed more time than the cap. - Detect signaled-kill via
err.killed/err.signal === "SIGTERM"in addition to the originalETIMEDOUTcode path.
Changed
- Default mra-ask timeout 120s → 300s. Live dogfood (2026-04-28) showed a complex 4-clause CJK question legitimately needs 160s of mra-internal LLM time; the v0.7.0 cap was killing healthy queries.
- Slack placeholder copy
(最多 2 分鐘)→(最多 5 分鐘)to match.
Caught by
A real escalate-flow turn with a multi-clause CJK whitelist question. Symptoms looked like "mra returned no results" but were actually pmk's premature SIGTERM. Manual reproduction of the same query: exit 0, 160s, perfect 3 KB answer.
[v0.7.4] — 2026-04-28 — atom approval (TTL hybrid)
GitHub release · PR #15 · closes #14
Added
KnowledgeAtomgainsstatus: "pending" | "approved"andexpiresAt?: number. Fresh atoms enterpendingwith a 24h TTL.pmk gateway atomsCLI:list [--all|--pending|--approved] [--scope <name>],show <id-or-prefix>,approve <id-or-prefix>,reject <id-or-prefix>. ID prefix matching: any unique prefix resolves.loadAtoms()auto-promotes pending atoms whoseexpiresAthas passed (idempotent on subsequent loads).
Changed
searchAtoms()now filters outstatus: "pending"atoms — pending content is invisible to retrieval until promoted.- Slack absorb confirmation message changed from "📚 已吸收..." to "⏳ 暫存為 pending, 24h 後自動生效..." with id prefix + approve/reject CLI hints.
Compatibility
Atoms written by v0.7.0–v0.7.3 have no status field on disk; the parser treats missing as approved so the existing corpus keeps working without rewrites.
Tests
141 → 148 (+7 covering pending exclusion, auto-promotion, approve/reject, prefix collision).
[v0.7.3] — 2026-04-28 — gateway dogfood follow-ups (round 2)
GitHub release · PR #13
Added
- Startup-time
mraWorkspacevalidation:runGateway()logs the workspace state at boot —mra workspace: <path>, a stale-warn, ornot configured … falling back to launch-cwd walk. Stale paths surface at startup instead of at first DM. MraAskResult.attemptsfield;runMraAskretries once on transient failures (no stderr, not timeout, not binary-missing). Matches the 2026-04-28 dogfood signature where a manual retry succeeded.- New
packages/cli/src/gateway/messaging.ts—buildIngestSeed,buildMraFailureMessage,buildMraSuccessMessage,truncateextracted fromslack/index.tsfor testability.
Tests
132 → 141 (+9 covering helper formatting, retry attempts, startup hooks).
[v0.7.2] — 2026-04-28 — gateway dogfood follow-ups (round 1)
GitHub release · PRs #11, #12 · closes #8, #9, #10
Added
GatewayConfig.mraWorkspace?: string— explicit absolute path to the workspace dir holding.collab/repos.json. Letspmk gateway startrun from any cwd.PMK_MRA_WORKSPACEenv override available for CI/containers.mraDoctor({workspace?})— explicit workspace wins when set AND valid; stale config returns a clear hint instead of silently falling through to cwd walk.pmk gateway initprompts for the path (auto-suggests detected workspace from cwd).pmk gateway statusshows configured path with(ok)/(no .collab/repos.json)marker.
Changed
- Failed
mra asknow surfaces stderr / partial stdout in both the gateway host log AND the LLM's apology context (viamra-stderr/mra-partial-stdoutfenced blocks). The model is instructed to cite the specific cause instead of a generic "unknown". pmk gateway escalation add/removeaccepts the canonical positionaldefault(no dashes); legacy--defaultform still works but emits a deprecation warning.- Slack userId validation in CLI (
^[UW][A-Z0-9]{2,}$) rejects typos like@hanfourearly.
Tests
119 → 132 (+13 covering config back-fill, env override, mraDoctor branches, escalate parsing, audience picker, runMraAsk hard-failure).
[v0.7.1] — 2026-04-27 — gateway prompt override
GitHub release · PR #7
Fixed
- Critical: live dogfood revealed the v0.7 directive layer (
mra-ask,escalate) was effectively dead.BASE_RULES(inherited by all gateway-DM prompts) opens with "you have NO tools, NO skills…" which contradicts theGATEWAY_TOOLBOXrules. Models defaulted to the safer no-tools rule and refused to emit directives. - Fix: prepend an explicit override at the top of
GATEWAY_TOOLBOXre-permitting the directive blocks for gateway-DM context.
Without this fix all the v0.7 plumbing worked in unit tests but the LLM never started the chain — the bot would say "I don't have access to the code" exactly when it should have asked pmk to run mra-ask.
[v0.7.0] — 2026-04-27 — pmk gateway (Slack bridge, Socket Mode)
GitHub release · PR #6 · ADR-0006, PRD-2026-0005
Added
pmk gatewayCLI verb withinit / start / status / statsplus the audience and escalation pool subcommands. Host runs the bridge in the foreground; users DM or@-mention@pmkfrom their existing Slack workspace.- Slack Socket Mode adapter (
@slack/socket-modev2). No public URL, no tunnel, no SaaS. Heartbeat-driven offline UX with:zzz:/:wave:broadcasts. - DM personal sessions + channel-shared cases persisted under
~/.pmk/gateway/slack/. - Per-thread session isolation — top-level DMs share a "main" session, each Slack thread gets its own.
- Channel free-chat fallback when no active case (with PKB grounding instead of refusing).
- Audience-aware prompts (
tech/biz/exec) — same answers, different tone. Per-user override. - Auto-mra-ask round — model emits a fenced
mra-askblock, pmk runsmra ask <repo>, synthesises with the result. - Escalate → absorb → retrieval — model emits
escalate, pmk@-mentions an IT contact, absorbs their reply as aKnowledgeAtom(~/.pmk/knowledge/<scope>/<slug>.md), retrieves it for future similar questions. - Slash commands inside Slack:
/pmk open|show|close|cases|help. - Honest offline UX — heartbeat file ticked every 30s; on stale (> 60s) or graceful shutdown, broadcasts presence change to recent conversations.
Security / hardening
- Path traversal sandbox for atom storage —
safeScope()strips everything outside[a-zA-Z0-9_-]at every entry point. Prompt-injectedrepo: ../../tmp/foolands astmp-foo, never escapes~/.pmk/knowledge/. - Bounded envelope LRU (2 000 entries) prevents memory growth on long-running hosts.
- gray-matter for atom front-matter — newlines / quotes / backslashes don't corrupt files.
- Race fix: pending-escalation marker claimed before LLM extraction (no duplicate atoms on fast IT replies).
- Timeouts — extractor + mra-ask both capped at 120s.
Tests
75 → 119 (+44 covering thread isolation, audience picker, escalate parser, atom round-trip, ranked search).
[v0.6] — 2026-04 — pmk case (long-lived bug investigation files)
PR #5
pmk case verb — symptom / hypotheses / evidence / next-questions persisted across sessions. The case-update fenced-block protocol becomes the foundation reused by v0.7's gateway flow.
[v0.5] — 2026-04 — pmk × mra bridge
PRs #2, #3, #4 · ADR-0005, PRD-2026-0004
pmk ingest mra:--all and pmk explore <repo> — code-intelligence work delegated to multi-repo-agent instead of growing pmk's own grep.
[v0.4] — 2026-04 — desktop app + full CLI
PR #1
Electron desktop app (chat panel + worktree manager). CLI verbs M0-M7: propose / draft / discuss / ask / debug / index / resume / worktree / tdd.
[v0.1–v0.3] — 2026-03 to 2026-04 — initial templates + traceability
Front-matter validation, Mermaid dependency graph, ADR / handoff / north-star templates, Confluence sync, Docusaurus docs site (EN + zh-TW). See git log for the early PRs.