You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The fleet has three renderings (CLI, dashboard widget, in-chat /fleet) and no team-visible one — "how is the fleet doing?" requires SSH. The unified-fleet spec (spec-20260818-150711, slice 1) makes Slack the fourth rendering; this issue is the product artifact that renders it.
Approach
A fleet digest subcommand in @amicode/amico-run: a pure formatter over the existing fleet registry plus configured machine probes, posting through the amico-slack CLI as a subprocess contract (no Slack API code in amico-run). Key reason: the shipped artifact is the verb, not the job — Notturno, cron, and on-demand are interchangeable triggers.
Approaches Considered
Slack API client inside amico-run — rejected: a second Slack client; the CLI already owns posting, voice, and footer logic.
Digest logic living in the Notturno workflow — rejected: Harmoniqs-only, not product.
Machine auto-discovery from ~/.ssh/config / tailscale — rejected: fleet topology would leak into product code; machines come from config.
(chosen) Formatter verb + subprocess posting via amico-slack.
Scope
In: the digest subcommand; machine probing from --machines / AMICO_FLEET_MACHINES; sessions summary from the fleet registry; --post channel resolution; full vitest suite. Out: the Node amico-slack bin port (separate issue — this verb only needs the CLI on PATH); the org projection (slice 3, gated on spec #318 Phase 2); any scheduler; "last seen" timestamps for down machines (needs state, v2).
Assumptions / Open Qs
amico-slack exists on PATH where the digest runs (at Harmoniqs: the mini, the fleet server). The Notturno wrapper injects its jobs rollup via an optional --jobs-line string so the product verb stays free of Notturno coupling.
Acceptance Criteria
amico fleet digest defaults to dry-run: prints the ≤6-line distilled block and the full table, exit 0.
Machines resolve --machines a,b → AMICO_FLEET_MACHINES env → honest "n/a" line when unconfigured; an unreachable machine renders ✗ and NEVER fails the digest (degrade-graceful).
Sessions render from the fleet registry (--root respected like the other fleet verbs); an empty registry renders "0 live sessions" honestly.
--post <channel> (or AMICO_SLACK_FLEET_CHANNEL) posts the block via amico-slack send <ch> --file, parses the emitted ts, then posts the table --thread <ts>.
amico-slack absent from PATH → errors-as-data (ok:false, exit 64), never a stack trace.
No channel names, hostnames, or Harmoniqs topology in the module — machines and channel are config.
pnpm --filter @amicode/amico-run test fleet_digest green; typecheck green.
Key Decisions
Posting is a subprocess contract: shell amico-slack, parse sent → … ts=<TS> from stdout for the thread reply. Keeps zero Slack deps in amico-run.
Registry read reuses readAllRecords from the fleet registry module — the digest never recomputes state (the projection invariant).
Machine probe: ssh -o BatchMode=yes -o ConnectTimeout=6 <alias> 'echo ok; hostname' — probe function injectable for tests.
Block posts even if the table post fails: the digest going out outranks the thread reply (ok:true + warning).
Optional --jobs-line "<text>" renders as the third block line — the Notturno wrapper supplies it; product users omit it.
Constraints & Invariants
Slack is a projection: the digest READS fleet verbs/registry and never writes a record.
The voice contract rides the CLI (app posts carry no footer); the verb adds none of its own.
Prior Art / Patterns
packages/amico-run/src/fleet_verb.ts — subcommand routing + errors-as-data idiom; header doctrine ("every fleet surface reads through THESE verbs").
packages/amico-run/src/fleet_registry.ts — readAllRecords, fleetRoot, purity-boundary house style.
packages/amico-run/test/fleet_verb.test.ts — temp-root test idiom.
~/.local/bin/amico-slack — the posting contract (send <ch> --file <f> [--thread <ts>], sent → … ts=).
Important
Problem
The fleet has three renderings (CLI, dashboard widget, in-chat
/fleet) and no team-visible one — "how is the fleet doing?" requires SSH. The unified-fleet spec (spec-20260818-150711, slice 1) makes Slack the fourth rendering; this issue is the product artifact that renders it.Approach
A
fleet digestsubcommand in@amicode/amico-run: a pure formatter over the existing fleet registry plus configured machine probes, posting through theamico-slackCLI as a subprocess contract (no Slack API code in amico-run). Key reason: the shipped artifact is the verb, not the job — Notturno, cron, and on-demand are interchangeable triggers.Approaches Considered
~/.ssh/config/ tailscale — rejected: fleet topology would leak into product code; machines come from config.amico-slack.Scope
In: the
digestsubcommand; machine probing from--machines/AMICO_FLEET_MACHINES; sessions summary from the fleet registry;--postchannel resolution; full vitest suite.Out: the Node
amico-slackbin port (separate issue — this verb only needs the CLI on PATH); the org projection (slice 3, gated on spec #318 Phase 2); any scheduler; "last seen" timestamps for down machines (needs state, v2).Assumptions / Open Qs
amico-slackexists on PATH where the digest runs (at Harmoniqs: the mini, the fleet server). The Notturno wrapper injects its jobs rollup via an optional--jobs-linestring so the product verb stays free of Notturno coupling.Acceptance Criteria
amico fleet digestdefaults to dry-run: prints the ≤6-line distilled block and the full table, exit 0.--machines a,b→AMICO_FLEET_MACHINESenv → honest "n/a" line when unconfigured; an unreachable machine renders✗and NEVER fails the digest (degrade-graceful).--rootrespected like the other fleet verbs); an empty registry renders "0 live sessions" honestly.--post <channel>(orAMICO_SLACK_FLEET_CHANNEL) posts the block viaamico-slack send <ch> --file, parses the emittedts, then posts the table--thread <ts>.amico-slackabsent from PATH → errors-as-data (ok:false, exit 64), never a stack trace.pnpm --filter @amicode/amico-run test fleet_digestgreen; typecheck green.Key Decisions
amico-slack, parsesent → … ts=<TS>from stdout for the thread reply. Keeps zero Slack deps in amico-run.readAllRecordsfrom the fleet registry module — the digest never recomputes state (the projection invariant).ssh -o BatchMode=yes -o ConnectTimeout=6 <alias> 'echo ok; hostname'— probe function injectable for tests.ok:true+ warning).--jobs-line "<text>"renders as the third block line — the Notturno wrapper supplies it; product users omit it.Constraints & Invariants
Prior Art / Patterns
packages/amico-run/src/fleet_verb.ts— subcommand routing + errors-as-data idiom; header doctrine ("every fleet surface reads through THESE verbs").packages/amico-run/src/fleet_registry.ts—readAllRecords,fleetRoot, purity-boundary house style.packages/amico-run/test/fleet_verb.test.ts— temp-root test idiom.~/.local/bin/amico-slack— the posting contract (send <ch> --file <f> [--thread <ts>],sent → … ts=).Source
specs/spec-20260818-150711-unified-amico-fleet-slack.md(personal vault, rev 2, commit 791aa4a) — slice 1 product half.