Append-only, newest entry on top. Never rewrite a past entry.
Added — ack manage: interactive menu for viewing/editing every agent
(2026-08-12, 0ee7233):
There was no re-enterable way to see every registered agent and change one
without re-running the whole ack configure wizard from scratch. ack manage lists every agent (registry-backed, or the single default workspace
when no registry exists) with live daemon status, then per agent: full
status report, list/create/delete habits, start/stop/restart the daemon,
re-run ack configure scoped to that workspace, and remove an agent from
the registry. Daemon start/restart poll the socket to confirm real
liveness instead of trusting a spawned pid or systemctl's exit code.
Root-mode daemon control is labeled as affecting the ONE shared
agent-enforcer.service, not just the selected agent. Also added ack habit delete. Menu-choice parsing and agent-list building are pure,
unit-tested functions (node/src/manage-menu.js); covered end-to-end by
node/tests/ack-manage.test.js (real spawned-process walks checking actual
file/registry/daemon state).
Fixed ack.js's ask() stdin helper in the same pass: it resolved on a
single raw data event and .trim()'d the whole chunk, so multiple
answers landing in one chunk (piped input, fast typing/paste) silently
collapsed into one garbage answer. Now properly line-buffers.
Fixed — ack/inject logs no longer default to /tmp (2026-08-12, d9866a4):
character.js, both ack_monitor.js/.py, and hermes_plugin/__init__.py
defaulted the ack-detection and habit-injection logs to /tmp/... —
tmpfs on many distros, doesn't survive a reboot, not a real
always-retrievable record. Now resolve into the persistent
<workspace>/.agent/ directory instead. Also: agent_enforcer_daemon.js's
_audit() now logs every tool_tick and submit_ack outcome (holds,
denial reasons, unknown-habit, reused-ack, cycle-complete) to
enforcer-audit.jsonl, not just tool-call allow/deny decisions.
Fixed — root-owned registry silently broke plain-user ack configure
(2026-08-12, bad9207):
resolveWorkspaces() set hasRegistry=true as soon as the registry file
existed, before attempting to read it — so a root-owned registry left
over from an unrelated root-mode deploy (unreadable, EACCES, to a plain
user) still forced a single-user ack configure into multi-workspace
socket naming, which nothing in ack.js's status/liveness checks looks
for — every liveness check reported the daemon dead even though it was
alive and correct. hasRegistry now only flips after a successful
read+parse. Full suite went from 94/104 to 104/104 passing once fixed.
Added — real multi-agent support (KD-21, 5 commits: b7a62bc, 152e5eb, 74ee90d, 5c1381b, and this doc/version pass):
One enforcer daemon now holds every agent on a machine, not one shared generic instance indistinguishable between them. drdeek's spec, refined through several corrections during the build: "how do you know if you have 12 agents running at the same time on one sock which one is doing what? It would get gridlock."
- Real per-agent identity resolution (
node/src/agent-identity.js, new): a real name for each agent's socket, resolved in order —agent.json'snamefield, thenSOUL.md(YAML frontmattername:, else its first#heading), then (for a known terminal harness — claude/hermes/opencode — not being used in a delegated multi-agent setup) the harness's own name with no prompt, then a direct interactive prompt, then"generic"as the last resort. Read-only — never writes to an identity file, never used in any enforcement decision. - Agents nest under one shared enforcer root, not their own top-level
workspace:
<enforcer-root>/agents/<agent-name>/, each with fully independentconstitution.yaml/enforcer.yaml/habits/— genuine per-agent tailoring (lighten/tighten enforcement per agent), not shared rules with per-agent naming as a coat of paint. - Sockets are named per agent, not a shared literal
enforcer.sockevery workspace used indistinguishably —<agent-name>.sock, consistent from the very first agent registered onward (multi-workspace mode is now forced by the mere existence of the registry, not just "more than one workspace," so agent #1's socket path never silently changes later when agent #2 gets added). deploy-agent-enforcer.shnow registers each deployed agent in a shared registry (/var/lib/agent-character-kit/workspaces.json, idempotent), and auto-restarts the enforcer + monitor when a new agent joins an already-populated registry (systemctl enable --nowis a no-op on an already-active unit, so without this a new agent's socket would silently never appear).- The monitor is now agent-aware: one process, but it tails every registered agent's own ack log independently and credits that agent's own socket — proven via a real end-to-end test (a real spawned daemon + real spawned monitor + a real 2-agent registry), including an explicit cross-contamination check.
ack status/doctor/repairnow report each registered agent individually instead of folding everything into one opaque "root (systemd)" entry.- The watchdog needed no changes — confirmed it already only checks "is the monitor process alive" / "is the enforcer process alive" via process-pattern matching, with zero per-agent assumptions baked in.
Fixed, found while building the above:
deploy-agent-enforcer.shlooked parameterized by$AGENT_WORKSPACEbut every seeding path actually hardcoded the literal string$VAR_DIR/workspace— passing a differentAGENT_WORKSPACEper agent would have silently seeded the same shared path every time regardless.- The registry path was originally derived from
dirname($AGENT_WORKSPACE), which for a nested agent path resolves one directory off from the fixed locationack statusand the daemon actually default to checking when no explicit env var is set (the normal case for a human running commands by hand, not through systemd's env). Introduced a real fixedACK_VAR_ROOT. deploy-ack-services.shindependently re-writes the same monitor/ watchdog unit filesdeploy-agent-enforcer.shalready wrote (real pre-existing duplication between the two scripts) — would have silently clobbered the registry env line since the documented flow runs both in sequence. Worked around by injecting the same env line in both places; untangling the duplication itself is still open.
32 new tests this pass, all confirmed passing individually, not just by aggregate count. 87/87 full suite passing.
Known gaps, explicitly not done: ack config has no registry
awareness yet. The Python monitor/watchdog equivalents (Hermes-only path)
weren't updated to match the Node monitor's rewrite. Repair can now see
which specific agent is down but doesn't yet selectively heal just that
one. Real systemd/sudo verification of the full chain still needs a human
— everything above is proven against real spawned processes in tests, not
against actual systemd.
Update, later the same day (10cf9c5, e6402cf, 8ed70c5): three of the
five gaps above are closed. deploy/ack_monitor.py ported to the same
multi-agent design as ack_monitor.js — verified live (real spawned
daemon + monitor against a real 2-agent registry, cross-contamination
check included) before the automated test was even written; 4/4 Python
suite passing. deploy-agent-enforcer.sh/deploy-ack-services.sh's
duplication untangled for real, not just worked around — the enforcer
script no longer writes or enables the monitor/watchdog units at all,
since it never installed their binaries in the first place;
deploy-ack-services.sh is now their sole owner. ack config show/verify/write-env all gained a real --agent <name> option,
registry-aware, verified live before the 5 new tests were written (ack config set deliberately left alone — pure passthrough, no per-agent
resolution to hook into). Still open: repair's selective per-agent
healing, and real systemd/sudo verification of the whole chain.
Update, later the same day (7b0b703): fail-safe audit of every
interactive prompt/flag in install.js + ack.js. Triggered by drdeek
live-catching the harness-selection menu silently wiring all detected
harnesses on blank Enter with no visible warning (fixed earlier same day,
6d08403). Same bug class found and fixed in 5 more places: the
privilege-mode prompt (blank Enter used to default to "1"/root+sudo,
the MOST privileged option — now requires explicit 1/2/3, no default at
all); the "no harness detected" fallback (blank input silently picked
"generic" with zero warning — now explains and requires a y/N
confirmation); configure --all (despite its own "Everything"
description, only ever installed ONE harness — now loops over all
detected harnesses, matching --yes); repair --reinstall (overwrote
existing habit files with zero confirmation — now lists exactly which
files get clobbered and requires y/N, with a --yes escape hatch for
scripted use); --harness help text (falsely implied cursor/gemini get
the same auto-detection/auto-naming as claude/hermes/opencode — they
don't, only hook generation). 103/103 Node + 4/4 Python passing.
Update, later the same day (8be35f6): root cause of a live root-mode
install self-lockout, found and fixed. drdeek ran a real root-mode
install (claude harness) to test the audit above; it wired the
PreToolUse/UserPromptSubmit hooks into his own live ~/.claude/settings.json
as designed, then every subsequent tool call in that same session started
failing — "Enforcer unavailable: enforcer socket not found" — fail-closed
by design, so killing the daemon or deleting the socket didn't help either;
recovery required manually stripping the hook keys back out of
settings.json (jq del(.hooks.PreToolUse, .hooks.UserPromptSubmit)).
Root cause, found by reading the deploy script directly rather than
guessing: deploy-agent-enforcer.sh correctly set up the socket's
directory (RUN_DIR, setgid + the ack-clients client group) but a
second, unrelated install -d on $AGENT_WORKSPACE/.agent — the same
path as RUN_DIR in the current per-agent architecture — ran right after
and silently clobbered it back to root:$SERVICE_GROUP/0750, no setgid.
The socket always came up root:root, unreachable by the agent's own uid
no matter how many times the client group was granted or the session
restarted. Fixed: reordered the two install -d calls so RUN_DIR's
cross-uid setup always wins; added a daemon-side chownSync-to-client-group
on every socket bind (agent_enforcer_daemon.js, new secureSocketFile()
helper) as defense in depth, independent of directory-setgid semantics,
threaded through via a new ACK_CLIENT_GROUP env var on the systemd unit.
Also fixed in the same pass: the misleading error message itself
(client.js's fs.existsSync() pre-check swallowed every stat error —
ENOENT, EACCES, anything — into the same generic "socket not found" text,
which is why a real permission-denied got reported as a missing file and
cost real diagnostic time; now connects directly and reports the real
err.code); the install summary's false "daemon + monitor + watchdog
already running via systemd" claim (never checked, and
deployRootIfNeeded() never actually deployed deploy-ack-services.sh at
all — acknowledgments silently never got credited; now actually deploys
monitor/watchdog if not already active, checked via systemctl is-active,
and reports real state either way); stale pre-multi-agent Workspace/Ack-log
paths in the summary (ignored inst.ws, the correct per-agent path, in
favor of process.env.AGENT_WORKSPACE or a hardcoded single-workspace
fallback left over from before the per-agent redesign). Also trimmed the
privilege-mode prompt from a 24-line wall of text repeating in full on
every single run to 5 lines with the same real information, after drdeek
called out over-explaining the least consequential part of the wizard
while under-explaining what the install summary actually did — full detail
pushed to AGENTS.md instead of inlined every time. 104/104 Node tests
passing. Still open, unchanged from above: repair's selective per-agent
healing, and a fresh real systemd/sudo verification of the corrected
socket-permission code specifically (the bug above was found via a real
systemd deploy; the fix itself has not yet been re-verified against one).
Update, next day (2026-08-08, eee4f95/9105f41/b752479/422f7fd):
the socket-permission fix above was wrong, and it took 4 more commits —
each one live-tested by drdeek, each one catching a real remaining bug —
to actually get it right. Full trace, honestly:
eee4f95— a real, separate bug found first: redeploying never restarted an already-running daemon (systemctl enable --nowis a no-op on an already-active unit), so no code fix could ever take effect on a redeploy without an unrelated manual restart. Fixed: redeploy now always restarts.8be35f6's fix was wrong, not just untested. It derived the cross-uid directory fromRUN_DIR="$(dirname "$ENFORCER_SOCKET")", assuming that equals$AGENT_WORKSPACE/.agent. Only true ifENFORCER_SOCKETis explicitly set to match — nothing in the real call chain does that, and the daemon never even reads it once a registry exists (routes tostartMultiWorkspaceDaemon()instead, which computes socket paths from the workspace itself). Confirmed live: fresh daemon restart, socket stillroot:root.9105f41— fixed the file's group correctly this time (root ack-clients, confirmed viasudo ls -la), by applying the setup directly to$AGENT_WORKSPACE/.agentinstead ofRUN_DIR. Still incomplete: a plainls(no sudo) still failed — the file's group was right, but traversal into its ancestor directories wasn't.b752479— fixed traversal on$VAR_DIR/$AGENT_WORKSPACE(0710, client-group execute-only). Still incomplete:namei -lon the real path showed the actual remaining block was at$ACK_VAR_ROOT(/var/lib/agent-character-kit) — a separate, higher ancestor, since$AGENT_WORKSPACEis nested three levels under it, not one.422f7fd— the real, complete fix: walks every directory level from$ACK_VAR_ROOTdown to$AGENT_WORKSPACE, granting client-group traversal regardless of nesting depth. Also caught two more real clobbers auditing the rest of the script: achown -Rthat reset the.agentdirectory's own group every deploy (now non-recursive, targets only the two config files it's meant to secure), and a second redundantinstall -don$ACK_VAR_ROOTlater in the script (registry setup) that re-clobbered the fix every single run.- Confirmed working, for real, live:
ls -la(no sudo) on the socket succeeded —srw-rw---- 1 root ack-clients .../claude.sock. One intermediate false alarm ("No such file or directory") turned out to be a pure timing race (lsran ~55ms after the daemon started, before its async socket bind finished) — confirmed viasudo findand the daemon's own "listening on..." log, not just assumed.
104/104 tests passing throughout — none of these four bugs could have
been caught by the JS test suite, since all four live entirely in
deploy-agent-enforcer.sh's bash/filesystem logic, which nothing in
node/tests/ executes for real. Lesson kept in memory
(rigor_no_half_assing): for anything with a real shell/deploy-script
component, code-reading confidence is not verification, even when the
reasoning is careful and unit tests stay green — only an actual run
against real system state proves it. Root-mode install is genuinely
trustworthy again as of 422f7fd.
Fixed (security-relevant):
- The enforcer's unix socket was locked to
0600(owner-only), which made root-mode's stated design ("agent can use it, can't tamper with it") impossible to actually use — a non-root agent could never connect to a root-owned owner-only socket. Fixed to0660/2750(group-restricted via a new sharedack-clientsgroup);ACK_AUTH_TOKENremains the real authorization check. Same bug was independently duplicated in a second socket-server implementation (multi-workspace mode) — fixed there too. package.json'sstartscript pointed at a nonexistent path (bin/ack.jsinstead ofnode/bin/ack.js) —npm starthas been broken since this script existed. Fixed and verified live.
Added:
- A real third privilege option: a dedicated, unprivileged service user
(default
ack-enforcer) as an alternative to full root — same real security boundary (different uid than the agent) without granting root. Both systemd deploy scripts now acceptACK_SERVICE_USER. - The interactive
ack configurewizard's privilege question — previously a silent binary root/no-root prompt — is now an explicit 3-way choice with real recommendations: system service (recommended), dedicated service user (recommended if root is undesired), trust-the-agent (explicitly labeled highly not recommended).
Known gap: none of the privilege-mode work above has been live-verified end to end — no sudo access during development (see blueprint KD-17). Syntax-checked only; needs real verification with real sudo.
Update, later the same day: the gap above is closed. drdeek ran a real
root-mode deploy, a full teardown (systemd units, service processes, npm
global package, workspace, Claude Code hooks — everything), and a fresh
reinstall, all with real sudo. Found and fixed live during that pass:
deploy-agent-enforcer.sh never installed the daemon's own npm
dependencies (KD-18); ack repair checked only one socket before
auto-activating a duplicate daemon (KD-20); status was silently gated
behind ACK_AUTH_TOKEN even though a bare CLI invocation has no token of
its own, which was the real reason KD-20's first fix looked like it didn't
work (KD-26).
Also fixed, same day (post-install/uninstall UX, all found via drdeek's own live testing, not review):
- The acknowledgment grammar's "resonates true because X" closer read as
an abstract truth-claim, not attribution to real work — replaced with a
<habit> <connector> <real work attribution>structure; the reason must now reference a real file/change, a past action actually taken, or a stated future effect. Verified via full git archaeology that this formula was described before but never actually implemented anywhere in this project's history. - The Python companion's optional
vectorsextra (numpy + sentence-transformers) is now a real opt-in prompt inack configure, only shown when the Python companion is actually selected; root mode auto-runs the realpip3 install(with a PEP 668--break-system-packagesretry) instead of only printing the command. .npmignorehad been sitting at the repo root the whole time, silently inert — npm only reads an ignore file from the actual package directory being packed (node/, or root depending on whichpackage.jsonis in play). A real dev-session audit log had been shipping in every published tarball as a result. Moved tonode/.npmignore, verified via a realnpm pack --dry-runbefore/after.- Removed the
postinstalllifecycle script entirely. Its only job was printing a pointer toack configure, and npm never reliably streamed that script's stdout to the real terminal — confirmed live: the script genuinely ran (its own fallback log proved it), but nothing appeared in the terminal. Tried writing straight to/dev/ttyas a workaround; it worked, but drdeek's call was to remove the script instead of carrying that workaround forward —ack status's existing first-run nudge (now the primary path, not a fallback) and the command listackitself prints cover the same ground without a lifecycle script's stdout reliability problems, and without triggering npm's separate (and, as of npm 11.18.0, permanently unsuppressable in this release)allow-scriptsadvisory warning at all.
Changed:
npm install -gnow NEVER configures anything, under any signal — supersedes the 1.2.1 entry below, whose described auto-configure-by-default postinstall behavior was itself replaced (blueprint CL-0005/MOD-005) before 1.2.1 actually shipped it, then corrected again today (CL-0007): theACK_YES=1bypass that could still make postinstall auto-configure has been removed entirely.npm install -gonly ever installs the package and prints what to run next; setup is always a separate, deliberate step.- The
installCLI command is renamed toconfigure(ack configure), matching the standard package-manager pattern (install via npm, then a separateconfigurestep, e.g.aws configure).ack installstill works as a backward-compatible alias. ack configure(no flags) is the real interactive step-by-step wizard.ack configure --yesis non-interactive, auto-detected sane defaults. Running nothing leaves the package fully inert.
Fixed:
ack.js's command handler force-appended--yesonto everyinstall.jsinvocation regardless of what was actually passed, so the genuine interactive wizard already implemented ininstall.js(readline-based, gated onopts.yes) was unreachable through the CLI —ack configuresilently ran non-interactively every time. Fixed; a new end-to-end test spawns the realack.jsbinary and confirmsack configure --yesreachesinstall.jsand starts a live daemon.
Also includes the accumulated Phase 0/1 work landed since 1.2.1: the
locational habit-file nudge, UserPromptSubmit hook wiring, preuninstall.js,
the ACK_AUTH_TOKEN spawn-env fix, daemon/monitor/watchdog liveness
verification surfaced as an install failure instead of a silent partial
success, the Claude-transcript acknowledgment detector, and the collapsed
single habit-creator module (fixing a real 3-way duplicate implementation).
See .blueprint/blueprint.md CL-0003 through CL-0007 for full detail.
Fixed:
postinstall.jsnow does a fully non-interactive, auto-detected setup onnpm install -g: detects which harnesses are actually present (Claude, Hermes, OpenCode; falls back to "generic"), wires a companion for each (merging Claude's PreToolUse hook automatically), and starts the daemon/monitor/watchdog for real —ack statusis alive immediately after install, no manual follow-up required. Replaces an earlier interactive postinstall wizard design, which was proven impossible: npm does not give lifecycle scripts a real TTY (confirmed by direct instrumentation), so a live prompt-and-wait wizard can never run there, on any package.install.js's non-interactive (--yes) path now accepts aharnessesarray alongside the existing singleharnessstring, so postinstall can set up every detected harness in onemain()call sharing one workspace/daemon/monitor/watchdog instead of spawning duplicates.ack statusnow surfaces thelooksNeverConfigured()fallback (previously dead code) for the case where lifecycle scripts were disabled (--ignore-scripts, or an org-wideallow-scriptspolicy) and postinstall never ran at all.- Root-mode auto-setup deliberately stays excluded from the unattended
postinstall path (needs a sudo password an unattended script must never
assume) — still requires the interactive
ack install, which gets a real terminal since it's a normal CLI invocation, not a lifecycle script.
Scoped tightly to avoid the exact footgun this package hit before (a script
literally named install that fired on any npm install, including local
dev): only proceeds when npm_config_global === "true" AND running from
inside a real node_modules install tree. Verified both directions live —
local dev install is a silent no-op, a real npm install -g auto-detects
every harness present on the test machine, stands up a genuinely running
daemon (not just configured-but-dormant), and correctly merges Claude's real
~/.claude/settings.json PreToolUse hook.
Fixed:
ack install's interactive root-mode prompt said "Requires sudo now" but never actually ran anything different — choosing root mode only changed a summary line at the end; the daemon/monitor/watchdog were still spawned as same-UID Node child processes either way. Root mode now actually runssudo bash deploy/deploy-agent-enforcer.shat that point in the flow, aborting cleanly with the real error if it fails.
Added:
- Multi-harness support: the interactive installer now loops asking "which harness(es)?" instead of taking exactly one, wiring a companion for each. Harnesses sharing the same workspace share one daemon/monitor/watchdog (provisioned once, not duplicated) — only the companion wiring repeats per harness.
- Existing-workspace discovery (
discoverAgentWorkspaces): per harness, the installer can scan a directory forSOUL.md/.agent/constitution.yamlmarkers and offer discovered workspaces to attach to, instead of only ever creating a fresh one. Bounded depth (6), skipsnode_modules/.git/build dirs, stops descending once a marker is found. CHANGELOG.mditself (this file) — first entry, per the convention documented inAGENTS.md§ Version tracking.
Tests: added discoverAgentWorkspaces unit tests (marker discovery,
skip-dirs, no-descend-into-found-workspace, empty/nonexistent root). Full
suite (17 tests) passes. New interactive flow (root-mode execution,
multi-harness loop, workspace sharing/dedup, scan-based discovery) proven
end-to-end via real pty-driven runs (expect) — piped/non-TTY stdin is
not a valid way to test this installer's multi-prompt flow (Node's
readline silently drops queued lines between question() calls against a
non-TTY pipe; confirmed as a piped-stdin artifact, not a logic bug, by
reproducing the same code correctly under a real pty).
Not recorded — this changelog starts from 1.2.0 forward, per AGENTS.md's own guidance not to block a release on backfilling pre-existing history.