Skip to content

Local-server mode + macOS support (auto-detected) — builds on #28 - #31

Merged
bborn merged 2 commits into
taskyou:mainfrom
kylecarbonneau:pr/local-mode-macos
Jun 9, 2026
Merged

Local-server mode + macOS support (auto-detected) — builds on #28#31
bborn merged 2 commits into
taskyou:mainfrom
kylecarbonneau:pr/local-mode-macos

Conversation

@kylecarbonneau

Copy link
Copy Markdown
Contributor

Builds on #28 (feat/channels-integration) — this PR is stacked on that branch, so the diff here is just these additions (3 files, +148/−13). Happy to fold them in as commits on #28 directly instead if you'd prefer; once #28 lands on main I'll retarget this to main.

Summary

#28 adds the push-based task-event channel, built for a remote Linux server reached over SSH. This makes the same channel also work when the GM runs on the same machine as the daemon — including macOS — auto-detected, with the remote-Linux/SSH flow left fully intact. Everything is additive; nothing existing is removed.

What changed

1. Channel local mode

templates/channel/taskyou-channel.ts.tmplrunRemote() gains a LOCAL branch: when SERVER_HOST is local / localhost / empty (IS_LOCAL), commands run via bash -lc directly on this machine (no SSH). Otherwise the original SSH path runs unchanged. ~/.local/bin is prepended to PATH and we cd into SERVER_HOME, mirroring the remote path.

2. Cross-platform hooks directory

setup.sh — the hooks dir was hardcoded to ~/.config/task/hooks, but ty's hooks.DefaultHooksDir() follows Go's os.UserConfigDir():

OS hooks dir
Linux ~/.config/task/hooks
macOS ~/Library/Application Support/task/hooks

A resolve_hooks_dir helper detects the OS via uname -s and picks the right path. Linux installs are byte-identical; macOS installs now land where the daemon actually looks (previously, hooks placed in ~/.config on a Mac silently never fired).

3. Local-server setup mode

setup.shsetup_server() detects a local server (SERVER_HOST local/localhost/empty) and delegates to a new setup_server_local() that configures git identity, registers project repos with ty locally, installs hooks to the OS-correct dir, creates notifications.jsonl, and skips SSH provisioning + the systemd service (GM and daemon share the box). The remote-Linux (setup_server) and exe.dev (setup_exe_dev) paths are untouched aside from adopting the OS-aware hooks dir.

Plus config.example.env documents SERVER_HOST=local.

Testing (macOS)

  • Rendered channel template compiles — bun build bundles cleanly and tsc --noEmit --strict is clean; the repo's own smoke-test.ts passes against the local render.
  • The local bash -lc runner drives ty commands without SSH.
  • Hooks install to ~/Library/Application Support/task/hooks and fire on task events.
  • Events surface in the GM session via the channel.
  • Remote-Linux/SSH path unchanged (additive else branches only — near-zero deletions).

Compatibility

Fully backward-compatible. Existing remote-Linux deployments behave exactly as before; the new behavior only engages when SERVER_HOST is local/localhost/empty or the target OS is macOS.

bborn added a commit that referenced this pull request Jun 9, 2026
notifications.jsonl survives as the push substrate, but the channels
refactor changes three things: copy taskyou-channel.ts (bun/TS) instead
of linear-poll.mjs; reuse assigned_gm (#32 + workflow#561) for per-GM
routing (answers the shared-bot vs per-user question); adopt the
IS_LOCAL/SSH runRemote pattern + OS-aware hooks dir (#31). Adds a
dependencies section and merge order (#28 -> #31/#32 -> Slack).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bborn
bborn force-pushed the feat/channels-integration branch from 8caee9e to 188555e Compare June 9, 2026 12:35
@bborn
bborn force-pushed the pr/local-mode-macos branch from a951d00 to d28e3bc Compare June 9, 2026 12:35
kylecarbonneau and others added 2 commits June 9, 2026 07:36
PR taskyou#28 assumes a remote Linux server reached over SSH. These additive
changes let the GM run on the same machine as the TaskYou daemon
(including macOS) with zero change to the existing remote-Linux flow:

- channel: runRemote() gains a LOCAL branch (bash -lc, no SSH) when
  SERVER_HOST is local/localhost/empty; the SSH path is unchanged.
- setup.sh: OS-detect the hooks dir to match ty's os.UserConfigDir()
  (Linux ~/.config/task/hooks vs macOS ~/Library/Application Support/
  task/hooks). The hardcoded ~/.config path silently misfired on macOS.
- setup.sh: a local-server provisioning path that skips SSH/systemd
  when the daemon runs on this same box; remote-Linux/systemd intact.
- config.example.env: document SERVER_HOST=local.

Both paths are auto-detected; no existing behavior is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Local mode registers projects and installs hooks against a ty daemon assumed
to run on this same machine. If ty is missing, fail loudly instead of erroring
mid-registration; if the daemon isn't running, warn — otherwise setup
'succeeds' but no task events ever fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bborn
bborn force-pushed the pr/local-mode-macos branch from d28e3bc to 5a64acd Compare June 9, 2026 12:36
@bborn
bborn changed the base branch from feat/channels-integration to main June 9, 2026 12:36
@bborn
bborn merged commit 0852e5f into taskyou:main Jun 9, 2026
bborn added a commit that referenced this pull request Jun 9, 2026
#28 (channels) and #31 (local/macOS) are merged to main, so the design
is concrete: a Slack module is taskyou-channel.ts with its two ends
re-pointed — reuse pollNotifications() + runRemote() verbatim; net-new is
just a Slack adapter + LLM classifier. Ship outbound (hook -> Slack
webhook) first, inbound control second. Removes the assigned_gm / per-GM
scoping dependency (out of scope: single bot, single operator).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bborn added a commit that referenced this pull request Jun 9, 2026
notifications.jsonl survives as the push substrate, but the channels
refactor changes three things: copy taskyou-channel.ts (bun/TS) instead
of linear-poll.mjs; reuse assigned_gm (#32 + workflow#561) for per-GM
routing (answers the shared-bot vs per-user question); adopt the
IS_LOCAL/SSH runRemote pattern + OS-aware hooks dir (#31). Adds a
dependencies section and merge order (#28 -> #31/#32 -> Slack).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bborn added a commit that referenced this pull request Jun 9, 2026
#28 (channels) and #31 (local/macOS) are merged to main, so the design
is concrete: a Slack module is taskyou-channel.ts with its two ends
re-pointed — reuse pollNotifications() + runRemote() verbatim; net-new is
just a Slack adapter + LLM classifier. Ship outbound (hook -> Slack
webhook) first, inbound control second. Removes the assigned_gm / per-GM
scoping dependency (out of scope: single bot, single operator).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bborn added a commit that referenced this pull request Jun 9, 2026
* docs: add Slack module design (manage TaskYou from Slack)

From TaskYou task #3723. Proposes modules/slack/, modeled on
modules/linear/linear-poll.mjs: watch a Slack channel/DM/@mention,
classify intent, drive ty, post replies, and tail notifications.jsonl
to push task.blocked/completed back to the channel. Enabled via
SLACK_ENABLED. Separates the chat-control bridge (recommended) from the
harder hosted/remote-MCP path (deferred to the credential-proxy work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: reconcile Slack design with in-flight channels work (#28/#31/#32)

notifications.jsonl survives as the push substrate, but the channels
refactor changes three things: copy taskyou-channel.ts (bun/TS) instead
of linear-poll.mjs; reuse assigned_gm (#32 + workflow#561) for per-GM
routing (answers the shared-bot vs per-user question); adopt the
IS_LOCAL/SSH runRemote pattern + OS-aware hooks dir (#31). Adds a
dependencies section and merge order (#28 -> #31/#32 -> Slack).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: land Slack design on merged channel work; drop per-GM scoping

#28 (channels) and #31 (local/macOS) are merged to main, so the design
is concrete: a Slack module is taskyou-channel.ts with its two ends
re-pointed — reuse pollNotifications() + runRemote() verbatim; net-new is
just a Slack adapter + LLM classifier. Ship outbound (hook -> Slack
webhook) first, inbound control second. Removes the assigned_gm / per-GM
scoping dependency (out of scope: single bot, single operator).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(slack): implement two-way Slack module (modules/slack)

Builds the Slack integration end to end (outbound + inbound), per the
design in docs/plans/2026-06-08-slack-module-design.md.

modules/slack/slack-bridge.mjs — zero-dependency Node daemon (raw fetch +
global WebSocket, like the Linear poller). Runs on the agents server next
to ty + notifications.jsonl:
  • Outbound: tails notifications.jsonl → chat.postMessage (blocked/
    completed/failed/started). Slack-originated tasks answer in-thread;
    everything else goes to SLACK_NOTIFY_CHANNEL.
  • Inbound: Slack Socket Mode (no public URL) → allowlist check →
    intent classification (Anthropic API, with a keyword-heuristic
    fallback when no key) → ty create/execute/input/list. Replies in-thread.

Also: unit tests for the pure logic (node --test, 11 cases), a
README, the ty-slack systemd service template, and setup.sh wiring
(server + exe.dev install the service; local mode renders files + .env)
plus SLACK_* in config.example.env and the README modules list. The
module is gated behind SLACK_ENABLED=true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(slack): default classifier to on-box `claude` CLI + runaway guards

Classifier no longer requires a separate ANTHROPIC_API_KEY: it prefers the
already-authenticated on-box `claude` CLI (claude -p — the same primitive the
GM/executors use), falls back to the Anthropic API if a key is set, then a
keyword heuristic. Override via SLACK_CLASSIFIER.

Loop / token-burn hardening (per review):
- claude -p sandboxed per call: --strict-mcp-config --mcp-config {} +
  --disallowedTools … keep it to a single turn (no agentic spiral);
  --max-budget-usd (default $0.05) hard-caps cost; timeout + maxBuffer; no retry.
- Self-loop guards: ignore own/bot/edited messages (bot_id/subtype/BOT_USER_ID).
- Concurrency cap (SLACK_MAX_CONCURRENT, default 3) bounds in-flight
  classifications; excess messages declined, not queued.
- Socket generation guard: only the newest connection's handlers stay live, so
  a reconnect race can't double-deliver events.
- Outbound poller: per-tick cap (SLACK_MAX_NOTIFS_PER_POLL, default 25) with
  byte-accurate gradual drain; only complete lines consumed (partial hook line
  held); lost/corrupt state skips backlog instead of replaying.

Tests: +2 (buildClassifierContext, readNewChunk partial-line/rotation) → 13/13.
Docs: README "Runaway / cost protection", config knobs, design-doc note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants