slack-manager-ai-helper helps managers turn noisy Slack traffic into repeatable follow-up workflows. You point it at the channels, DMs, and people you care about, teach it what counts as signal, and it produces a durable local record plus an LLM-ready summary only when something worth reviewing actually happened. The included examples cover plan reviews and actions follow-up, and the same pattern fits other manager workflows such as escalation watch: for example, tracking delivery-risk or outage-related updates across engineering channels so a lead can review blockers without reading every thread.
It is designed for teams that already rely on Slack as the place where plans, asks, status changes, and decisions happen. Instead of asking a model to read everything, this tool first finds candidate messages deterministically, expands surrounding evidence, then runs analysis on the compact, redacted context. The result is a practical manager feed you can schedule, inspect, archive, and reuse as memory on later runs.
Technically, this is a TypeScript CLI that reads a local Slack SQLite database produced by slacrawl. It opens that database read-only, matches configured messages, expands context, calls an LLM through the Vercel AI SDK only when matches exist, and persists run windows, evidence, memory, and model output in a local JSON state file under tmp/ by default.
- Plan reviews: watch a planning or delivery channel for daily plans, then score whether they are detailed enough for predictable execution. See
examples/plan-reviews/README.md,examples/plan-reviews-minimal-config.json, andexamples/plan-reviews-config.json. - Actions follow-up: watch manager mentions, DMs, MPIMs, and selected channels for asks that became trackable actions. See
examples/actions-followup/README.md,examples/actions-followup-minimal-config.json, andexamples/actions-followup-config.json. - Escalation watch: a plausible third pattern is tracking messages that look like blockers, risks, or incident escalations so a manager can review what needs intervention without tailing every conversation in real time.
Matchers decide which messages become anchor messages. Only after anchors are found does the tool expand nearby evidence and optionally call the analysis model.
Supported matcher types:
regex: JavaScript regular expression against Slack message text.text: simple term containment; any term can match.mention: direct one-user mention matching.scored: weighted heuristic scoring, optionally backed by embeddings and/or a classifier.and/or: positive matcher composition.exclude: top-level suppressor; when its nested positive matcher matches, the message is ignored.
Processing behavior:
- Messages are read from the slacrawl database, normalized, redacted, and filtered for non-empty text before any matcher sees them.
- For each eligible message, the tool evaluates all applicable top-level
excludematchers before positive matchers, regardless of config order. - If an
excludematcher matches, the message is ignored and no evidence or state entry is recorded for it. - If no
excludematcher matches, positive matchers are evaluated in order: global matchers first, then channel-local matchers. - Positive matching stops at the first match for that message. The first matching matcher id/type is recorded as the anchor reason, and later positive matchers are not evaluated for that message.
- A configured channel with no applicable positive matchers matches every eligible message in that channel. This is useful for low-volume or topic-dedicated channels.
- Channel
usersfiltering and matcher success are an AND for anchor selection: if you restrict a source to certain users, a message must come from one of those users and then either match at least one source/global matcher or fall through a configured channel with no applicable positive matchers. - Global matchers never match everything by default. When
channelsis omitted, global matchers scan every Slack source, but a message still needs an explicit global matcher hit. To intentionally match every Slack source, configure a catch-all matcher such as aregexmatcher with pattern.*.
About mention matching:
- It is for one Slack user id, configured as
userId. - It matches Slack-formatted mentions like
<@U12345678>. - It also accepts literal
@U12345678text if that exact user id appears in the message. - It does not resolve Slack user groups such as
@eng-leads,@support, or other workspace aliases. If you need those, usetext,regex, or ascoredmatcher.
Redaction happens at the Slack read boundary.
- Slack messages are redacted immediately after they are read from the slacrawl SQLite database.
- Every matcher sees the post-redaction text, including
regex,text,mention,scored,and,or,exclude, embeddings inputs, and classifier prompts. - Context expansion, logs, persisted evidence, previous memory passed back into the model, and the final report/memory output are all handled from redacted text.
- Messages that become empty, whitespace-only, or exactly
[REDACTED]after normalization and redaction are dropped before matching.
This means you should think of the readable Slack database as the only place where original text exists in this workflow. Once a message enters slack-manager-ai-helper, downstream processing is meant to stay redacted.
When a message matches, the tool stores both the match result and the evidence that surrounded it.
Evidence can include:
- the matching message itself (
source: "match") - real Slack thread replies (
source: "thread") - nearby same-channel messages before and after the match (
source: "nearby") - top-level messages that score as synthetic related context (
source: "synthetic_related")
Persisted state is a local JSON file, usually state-<topic>.json beside the config file. State
uses a run-centric v1 shape:
runs: each execution attempt and whether it completed or failedruns[].executionMode:explicitfor date/window-bounded invocations,implicitfor unbounded cron-style invocationsruns[].requestedRange: the explicit local-date range when one was requestedruns[].scanStartCursor/runs[].scanEndCursor: the Slack timestamp cursor bounds used for the scanruns[].memories: durable memory produced by that runruns[].modelOutputs: raw model output, extracted report text, parsed JSON, validation results, and the model provider/name/attempt metadata that produced the reportruns[].evidenceMessages: the redacted Slack evidence used for that run's model call
State files can be compacted when history grows too large:
slack-manager-ai-helper compact-state --config examples/plan-reviews-config.json --keep-runs 20This keeps the last N full runs, prunes model outputs and evidence for older non-retained runs,
keeps memory-bearing run history needed for future memory continuity, writes a timestamped .bak
backup by default, and prints a JSON summary. Use --no-backup to skip backup creation.
Useful inspection commands with jq:
jq '.runs | map({id, status, executionMode, scanStartCursor, scanEndCursor, matchedMessageCount, modelCalled, finishedAt})' examples/state-plan-reviews.jsonjq '.runs[-1].evidenceMessages | map({runId, source, channelId, ts, userId, text})[:20]' examples/state-plan-reviews.jsonjq '.runs[-1].modelOutputs | map({runId, createdAt, modelProvider, modelName, modelAttempts, schemaValid, reportText})' examples/state-plan-reviews.jsonjq -r '[.runs[].memories[]] | last | .content' examples/state-plan-reviews.jsonslack-manager-ai-helper does not talk to the Slack API directly. It uses slacrawl as the capture layer and treats the resulting SQLite database as the system of record for Slack history.
- The helper opens the slacrawl SQLite database read-only and never writes to it.
- The only time it calls the
slacrawlCLI itself is to discover the default database location withslacrawl metadata --jsonwhenstorage.slacrawlDatabasePathis not configured. - In practice, this lets you separate capture from analysis. One machine can stay logged into Slack and keep the database fresh, then that database can be copied or exported to another machine that only runs analysis.
Operationally, slacrawl is also where Slack access details live:
- If you are using
slacrawl tail, that is the layer to think about for Slack API tokens, app bot user keys, and capture health. - Watch for capture-side problems such as a desktop wiretap session not running, API keys not being available, or the Slack desktop app not being open when your slacrawl setup depends on it.
- If the database is stale,
slack-manager-ai-helperwill faithfully analyze stale data, so freshness checks belong at theslacrawllayer. - Slackbot, bot-message, and common Slack system-event rows are skipped at the slacrawl read boundary before matching, context expansion, evidence persistence, or model input.
Use nvm to select the Node version from .nvmrc, then install dependencies:
nvm use
pnpm installInstall slacrawl separately for your operating system, for example:
brew install slacrawlConfigure provider credentials in .env. Typical variables are OPENAI_API_KEY,
ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, XAI_API_KEY, and OPENROUTER_API_KEY.
LOG_LEVEL controls Pino logging and defaults to info. Set TZ to the local timezone used for
dates and times exposed to the LLM. The compact LLM state groups messages by local date and sends
message times as HH:MM, for example with TZ=America/Sao_Paulo. The CLI logs a warning and falls
back to the system local timezone when TZ is unset.
pnpm run slack-manager-ai-helper run --config examples/plan-reviews-config.json
pnpm run build
pnpm run lint
pnpm run test
pnpm run slack-manager-ai-helper run --config examples/plan-reviews-config.jsonThe bundled CLI supports:
slack-manager-ai-helper run --config examples/plan-reviews-config.json
slack-manager-ai-helper run --config examples/plan-reviews-config.json --date today
slack-manager-ai-helper run --config examples/plan-reviews-config.json --date 2026-06-03
slack-manager-ai-helper run --config examples/plan-reviews-config.json --window previous-5-workdays
slack-manager-ai-helper run --config examples/plan-reviews-config.json --start-date 2026-06-01 --end-date 2026-06-03
slack-manager-ai-helper collect-model-input --config examples/plan-reviews-config.json
slack-manager-ai-helper unified-report --config examples/plan-reviews-config.json --prompt examples/plan-reviews/WEEKLY_REPORT.md --days 5
slack-manager-ai-helper run-portfolio --manifest examples/portfolio-plan-reviews.json --dry-run
slack-manager-ai-helper run-portfolio --manifest examples/portfolio-plan-reviews.json --analysis plan-reviews --target project-alpha --date today --no-notify
slack-manager-ai-helper run-portfolio --manifest examples/portfolio-plan-reviews.json --due --concurrency 4
slack-manager-ai-helper manage-portfolio --manifest examples/portfolio-plan-reviews.json
slack-manager-ai-helper create-config --reference examples/plan-reviews-minimal-config.json --output my-topic-config.json
slack-manager-ai-helper validate-config --config examples/plan-reviews-config.json
slack-manager-ai-helper run --config examples/plan-reviews-minimal-config.json
slack-manager-ai-helper run --config examples/actions-followup-config.json
slack-manager-ai-helper run --config examples/actions-followup-minimal-config.json
slack-manager-ai-helper inspect-config --config examples/plan-reviews-config.json
slack-manager-ai-helper compact-state --config examples/plan-reviews-config.json --keep-runs 20
slack-manager-ai-helper resolve-evidence --config examples/plan-reviews-config.json --id '2026-06-05T12-00-00-000Z:0'
slack-manager-ai-helper resolve-evidence --config examples/plan-reviews-config.json --id 'e1a4dcb8-aa76-4e2a-8594-00f331ab3c69' --output jsonrun-portfolio executes analysis tasks with the existing runOnce() runtime, attached rollups with
the existing unified-report runtime, and maintenance tasks such as compact-state over the same
target state files. Pass --date to rerun one local day and plan only tasks whose schedule matches
that day. Pass --due for cron-style execution that filters by the current local day and schedule
time. When publishing is enabled, it writes markdown reports to the configured report
archive and updates latest views for current windows. When reports.commit.enabled is true,
run-portfolio creates one git commit after all selected tasks finish for touched report files,
rollup report files, analysis state files, and maintenance state/backup files that live under the
archive repo. Only paths that git status reports as changed are staged and committed; gitignored
paths such as maintenance *.bak backups are skipped so a partial git add cannot abort the commit. Pass --no-publish for state-only execution. When notifications are enabled, it
passes the materialized runAndNotifyConfig to run-and-notify as dotted CLI arguments, forces
markdown stdout, and delivers the saved report via cat <report>. Pass --concurrency N to run up
to N analysis/target lanes in parallel; tasks for the same analysis target still run in plan order
so rollups and maintenance do not overtake that target's analysis run.
Portfolio defaults.matchers.pre and defaults.matchers.post compose common matcher objects into
every materialized channel matcher list, before validation with schemas/config.schema.json. Use
post for shared suppressors such as common exclude matchers. If a channel has no local
matchers, the common matchers become that channel's matcher list. runAndNotifyConfig.name is
optional in portfolio manifests and defaults to the target display name; override it only when the
notification label should differ from the target name.
manage-portfolio is an interactive manifest editor for private ops manifests. It can list the
portfolio, add analyses and targets, edit target channels with the same guided channel/user/matcher
flow as config editing, edit members on a selected target channel (add/remove) from the edit-target
submenu, move a member between targets (remove from every source channel and add to every destination
channel), add optional target-level model and scored-matcher provider overrides, edit analysis
defaults and target overrides as JSON, add runs, rollups, and compact-state maintenance tasks,
pause/resume/archive targets, configure runAndNotifyConfig with guided SMTP/Slack transport prompts
or raw JSON at portfolio/analysis/target scope, validate, and preview the same dry-run plan as
run-portfolio --dry-run. Portfolio-default guided notification setup collects full transport
settings; analysis and target guided setup collects only override fields such as SMTP to and Slack
defaultChannel because those blocks deep-merge with parent config. Guided Slack setup can disable
classic link/media previews via transports.slack.unfurlLinks and transports.slack.unfurlMedia
(false/false), which run-and-notify forwards to Better Notify. Submenus include a back option so
edit flows can return without saving changes. New target setup seeds user roles from sibling targets when available. New schedule
prompts default to 22:00. Saving validates the manifest first and writes a timestamped .bak
beside an existing manifest before replacing it. Pass --create when the manifest path does not
exist and you want to start a new manifest.
To build a new config interactively from an existing one, use:
slack-manager-ai-helper create-config --reference examples/plan-reviews-minimal-config.jsonTo edit an existing config in place with the same guided channel, user, role, matcher, and preview flows, use:
slack-manager-ai-helper edit-config --config examples/plan-reviews-config.jsoncreate-config prompts for an output path when --output is omitted. It inspects the configured
read-only slacrawl database to offer searchable channel and user choices with known names, source
kinds, and private flags when those are present in slacrawl metadata. If channel membership metadata
is unavailable, mention-user choices fall back to users who sent messages in that channel.
edit-config edits the selected config in place, optionally accepts --reference for copyable
roles and matchers, validates before saving, and writes a timestamped .bak beside the original.
The command seeds prompts, model settings, channels, users, roles, matchers, context, and provider
defaults from --reference. Built-in prompt tokens such as @DEFAULT_BASE_INSTRUCTIONS@ remain
references. For prompt files, the wizard asks whether to reference, copy, or copy-and-edit; edits are
always made to a copied file near the output config, never to the original reference prompt.
Optional top-level arrays such as channels and globalMatchers are omitted when empty instead of
being written as empty arrays.
Matcher try-it flows and final preview use the same redacted slacrawl read boundary as run, so
sampled real Slack text shown by the wizard has already passed through configured redaction. Scored
matcher and synthetic-thread assistance can send user-entered samples to the configured main model,
but only after confirmation; the model suggestions are editable before saving.
After writing the config, the wizard can preview real matching without changing state. This preview shows scanned counts, matched anchors, evidence that would be sent to the model, and ignored messages with reasons. It can also dry-run the analysis model and render the markdown report in terminal colors. Preview and dry-run never create runs, save evidence, write model output, or save memory.
Local Ollama helper scripts:
pnpm run ollama:classifier
pnpm run ollama:embeddingsBoth helpers start an Ollama server at http://127.0.0.1:11434 when one is not already reachable,
pull the configured model, and run a small smoke request. Override defaults with environment
variables:
OLLAMA_CLASSIFIER_MODEL=qwen3:0.6b pnpm run ollama:classifier
OLLAMA_EMBEDDINGS_MODEL=nomic-embed-text pnpm run ollama:embeddings
OLLAMA_URL=http://127.0.0.1:11434 pnpm run ollama:embeddingsOptional live Ollama scoring integration tests are not part of normal QA:
pnpm run test:ollamaThese live tests run sequentially because local Ollama models can be CPU or memory constrained.
Override the defaults with OLLAMA_TEST_TIMEOUT_MS for each Vitest case and
OLLAMA_PROVIDER_TIMEOUT_MS for each Ollama HTTP request.
All configuration files are JSON and are validated by schemas/config.schema.json. Relative paths
are resolved from the config file directory. ~ is expanded to the current home directory.
Top-level fields:
workspaceUrlis required and must be the HTTPS base URL for the Slack workspace, such ashttps://example.slack.com. Evidence links are generated as<workspaceUrl>/archives/<channelId>/p<tsWithoutDot>for top-level messages, with?thread_ts=<threadTs>appended for thread replies so they can open in Slack's thread view. They remain HTTP links so they work in email clients that stripslack://app links.storageis optional.storage.slacrawlDatabasePathoverrides the default slacrawl database path; when omitted, the CLI runsslacrawl metadata --jsonand usespaths.default_database.storage.statePathoverrides the default local state file path.promptsis required and must be either one prompt reference or an ordered array of prompt references.@DEFAULT_BASE_INSTRUCTIONS@and@DEFAULT_BASE_PROMPT@expand to built-in prompts; other prompt references are Markdown paths resolved relative to the config file. Prompt content is concatenated in order.modelis required and must includeproviderandmodel. Optional fields arefallback,retries,baseUrl,temperature,reasoningEffort,maxOutputTokens,contextWindowTokens,timeoutMs,openrouter,minReportWords, andfailOnInvalidOutput.fallbackis a recursive model config with the same fields; the runtime tries it after the current model exhausts its retries because provider execution failed orfailOnInvalidOutputrejected structured output.retriesdefaults to3for each model, so a model can be called up to four times before falling back or failing.baseUrlconfigures providers that support custom endpoints, such asopenai-compatibleandollama.timeoutMsaborts the analysis model call after the configured number of milliseconds.openrouteris only used whenproviderisopenrouter; omitted OpenRouter subfields are not sent. Setopenrouter.includeReasoningtofalseto sendreasoning.exclude: true, or totrueto sendreasoning.exclude: false.openrouter.orderandopenrouter.allowFallbacksare forwarded as OpenRouter provider routing settings. WhenfailOnInvalidOutputis true and a prompt declares an output JSON Schema, schema-invalid model responses are retried according to that model'sretriesvalue before the runtime triesfallbackor fails the run. WhenfailOnInvalidOutputis false, invalid output is stored with validation errors instead of triggering fallback. Configuredtemperatureis omitted automatically for known reasoning model families, such asgpt-5...ando1/o3/o4..., because those models do not support temperature.reasoningEffortmay benone,minimal,low,medium,high, orxhigh, and is passed through to providers/models that support reasoning effort.minReportWordsdefaults to25; structuredreportTextmust contain at least that many meaningful words, where a meaningful word is a run of four or more alphabetic characters after Markdown formatting, links, URLs, code fences, and punctuation are removed. Built-in analysis provider ids areopenai,anthropic,google,xai,openrouter,opencode,openai-compatible,gateway, andollama. WhencontextWindowTokensis set, the runtime estimates the fully compiledsystem + prompttokens, including configured prompts, minified schemas, compact state, and previous memory, reservesmaxOutputTokensor a conservative default for output, and splits model calls by local date/channel before sending a request that would exceed the configured window. When fallbacks are configured, segmentation uses the smallest configuredcontextWindowTokensand the largest configuredmaxOutputTokensacross the fallback chain.redactionis optional and enabled by default. Optional fields areenabled,replacement, andadditionalPatterns; custom patterns use JavaScript RegExp source strings, optional flags, and always run with global/unicode flags.channelsis optional. When provided, it must contain at least one channel object withid. Optional channel fields arename,kind,alsoChannels,users, andmatchers;kindmust bechannel,dm,mpim, orunknown.channels[].alsoChannelsis optional. When provided, it is a non-empty array of additional Slack sources that reuse the parent channel'susersandmatchers. Each entry requiresidand may setnameandkind. At load time the loader expands each entry into a full channel config; omittedkindinherits the parent channel kind when present. Expanded channel ids must be unique across the whole config, including primary channel ids.channels[].matchersis optional. When a configured channel has no applicable positive matchers from eitherglobalMatchersorchannels[].matchers, every eligible message in that channel becomes an anchor match after user filtering and excludes.channels[].usersrestricts anchor matching for that source: when users are configured, a message must be from one of those users and then either satisfy a source/global matcher or fall through a configured channel with no applicable positive matchers. Context expansion can include thread, nearby, and synthetic-thread messages from any user.globalMatchersis optional and applies across all configured channels. Whenchannelsis omitted, global matchers scan every Slack source in the slacrawl database. They do not match all messages by omission; use an explicit catch-all matcher such as aregexmatcher with pattern.*if that is intended.- Provide
channels,globalMatchers, or both. A config with neither is valid but cannot match any Slack message. scoredMatcherDefaultsis optional. It provides defaultembeddingsandclassifierprovider settings for everyscoredmatcher; matcher-local provider settings override these defaults.contextis optional and behaves like{}when omitted. Optional fields areincludeRealThread,nearbyMessagesBeforeMinutes,nearbyMessagesAfterMinutes,maxMessages, andsyntheticThreads.- Context defaults are
includeRealThread: true,nearbyMessagesBeforeMinutes: 5,nearbyMessagesAfterMinutes: 60, andmaxMessages: 100.maxMessageslimits same-channel nearby context per matched anchor; the count resets for every match and does not cap matched anchors or real thread messages. context.syntheticThreadsis optional. It supportsenabled,candidateWindowMinutes,thresholds,scoring,heuristics,embeddings, andclassifier.- Synthetic-thread detection is enabled by default; set
context.syntheticThreads.enabledtofalseto disable it. Synthetic-thread scoring defaults tocandidateWindowMinutes: 60, thresholds0.6/0.6/0.72/0.72, and weightssemanticSimilarityWeight: 0,lexicalOverlapWeight: 0.55,sameAuthorOrMentionWeight: 0.2,temporalProximityWeight: 0.2, andchannelTopicMatchWeight: 0.05. - Synthetic-thread detection skips same-channel candidates that would already be selected by a configured channel's match-all behavior, so provider calls focus on otherwise-unmatched context such as unconfigured users replying in a monitored channel.
context.syntheticThreads.classifier.useForRangesis optional and must contain at least one ofambiguous,unrelated, orrelatedwhen provided. It defaults to["ambiguous"].- Scored matchers default to thresholds
0.6/0.6/0.72/0.72and weightssemanticSimilarityWeight: 0,questionSimilarityWeight: 0.15,keywordWeight: 0.45,phraseWeight: 0.3, andpatternWeight: 0.1. - Synthetic-thread scoring uses top-level Slack messages as anchors and candidates.
Matcher types:
regexrequiresid,type, andpattern; optionalflagsuses JavaScript RegExp flags and defaults toiuwhen omitted.textrequiresid,type, and non-emptyterms;caseInsensitiveis optional.mentionrequiresid,type, anduserId; it matches Slack<@USER>mentions and literal@USERIDtext for that exact user id.scoredrequiresid,type, andquestion. It scores a single message with optionalthresholds,scoring,heuristics,embeddings, andclassifierblocks. It combines deterministic heuristics with AI SDK embeddings and classifier calls when enabled, then treatsrelatedscores as matches. This is useful for questions such as "is this an action?"andrequiresid,type, and non-emptymatchers; every nested positive matcher must match.orrequiresid,type, and non-emptymatchers; any nested positive matcher can match.excluderequiresid,type, andmatcher. It is allowed only at the top level ofglobalMatchersorchannels[].matchers; the nestedmatchermust be a positive matcher expression, soexcludecannot be nested insideand,or, or anotherexclude.
Slack source kinds:
channelis a public or private Slack channel.dmis a one-to-one direct message. Slack's API commonly calls this animconversation.mpimis a multi-person direct message, sometimes shown as a group DM.
Any configured prompt file may include one fenced jsonschema block. When present, that schema
defines the structured output memory field, and the analysis model is called through AI SDK
structured output with a top-level { "memory": ..., "reportText": "..." } object. The full
object is still validated locally with Ajv, and reportText is additionally checked as non-empty
Markdown that does not contain memory/jsonschema fenced blocks. Invalid output is stored with
validation errors; the run only fails when model.failOnInvalidOutput is true. In that mode,
schema-invalid responses are retried with the validation errors according to model.retries before
the runtime tries model.fallback or fails.
As a context-reduction optimization, every configured prompt file is scanned before the model call
for fenced code blocks tagged json or jsonschema. Those blocks must contain valid JSON; valid
blocks are minified before being sent to the model.
The state file is created automatically at storage.statePath. When storage or statePath is
omitted, it defaults to state-<config-name>.json beside the config file, with a trailing
-config removed from the config filename. For example,
examples/planning-review-project-xpto-config.json defaults to
examples/state-planning-review-project-xpto.json.
The topic id is inferred from the config filename with the same -config suffix rule, so
examples/plan-reviews-config.json uses topic id plan-reviews. Scheduling belongs to whatever
invokes the binary.
The slacrawl database path defaults to paths.default_database from slacrawl metadata --json and
is opened read-only. Structured logs include trace context such as topic id, SQL-detected
workspace id when available, channel id, user id, matcher id, redacted message text snippets,
scoring ranges, provider/model settings, and model input sizes. Set LOG_LEVEL=debug to include
the compiled model input and redacted model output in logs. Workspace id is inferred from
slacrawl SQLite metadata such as message-table workspace_id/team_id columns or workspace/team
tables. Debug logging is useful while setting up prompts and configuration because it traces the
compiled model payload and model output, but reduce LOG_LEVEL to warn for real usage. The
compact LLM input state is validated against schemas/state.schema.json and sends the config
topicId plus evidence as a nested timeline: date, day of week, per-day configured-user
summaries, channel, thread, and messages with local time: "HH:MM". Daily user summaries report
status: "present" with firstMessageAt, channelMessages, repliesToOthers,
totalMessages, authoredTopLevelMessages, authoredReplies, and ownedEvidence, or status: "absent" with no counters. channelMessages counts top-level evidence messages in configured
Slack sources, repliesToOthers counts evidence reply messages in known threads started by another
user, and totalMessages equals authoredTopLevelMessages.length + authoredReplies.length.
Authored message arrays contain Slack HTTP permalink hrefs that match entries under
channels[].threads[].messages[]. ownedEvidence lists authored messages from matched anchors and
real thread context that prompts may use as that configured user's own planning, update, review, or
completion evidence; nearby and synthetic context messages are excluded. Messages
include userName from config or the slacrawl directory when known, configured userRole when
known, externalAuthor: true for known authors outside the configured users, evidenceScope: "owned" | "context", anchor: true when the row was a matched anchor before context expansion,
and href as a clickable Slack HTTP permalink. Reply hrefs include ?thread_ts=<threadTs>.
Matcher ids, match reasons, raw evidence source tags, run id, and full timestamps are omitted from
the LLM input.
To inspect the exact compiled system and prompt payload without calling the model, use:
slack-manager-ai-helper collect-model-input --config examples/plan-reviews-config.jsonThis command is read-only: it does not create a run record, save evidence, write model output,
or save memory. When a model call would happen, stdout is JSON with
modelInput.system and modelInput.prompt. When no model input would be generated, stdout is
still JSON and includes a reason, such as no_matches.
Use --state-only to stop before prompt compilation and print the compact JSON state that would
be embedded in the model prompt:
slack-manager-ai-helper collect-model-input --config examples/plan-reviews-config.json --state-onlyTo synthesize a weekly or multi-day report from already stored daily model outputs, use
unified-report with a separate rollup prompt:
slack-manager-ai-helper unified-report --config examples/plan-reviews-config.json --prompt examples/plan-reviews/WEEKLY_REPORT.md --days 5
slack-manager-ai-helper unified-report --config examples/actions-followup-config.json --prompt examples/actions-followup/WEEKLY_REPORT.md --start-date 2026-06-01 --end-date 2026-06-05This command is read-only: it does not scan slacrawl, create runs, write model outputs, or save
memory. --days N selects today and the previous N - 1 local dates using TZ;
use --date, --window, or --start-date/--end-date for explicit local-date ranges.
--state-only prints the assembled source-report JSON without calling the model.
run writes only the extracted reportText to stdout, so it can be piped directly to notification
tools. Metadata, scan cursors, evidence, memory, raw redacted model output, and schema validation
status are stored in the state file.
To resolve a stored evidence id back to the Slack coordinates used for analysis, use:
slack-manager-ai-helper resolve-evidence --config examples/plan-reviews-config.json --id '2026-06-05T12-00-00-000Z:0'The --id value can be either a persisted evidence id in run-id:index form, such as
2026-06-05T12-00-00-000Z:0, or a unique Slack/slacrawl messageId UUID that appeared in model
output. The default --output link prints the configured Slack HTTP permalink. Use --output json
for scriptable output that also includes the redacted stored evidence text, how the id was resolved,
the state file used, permalink/reference fields, configured channel name/kind when available, and a
clearly labeled slack-reference coordinate with channel id, timestamp, optional thread
timestamp/message id/user id, run id, and evidence id.
When model.contextWindowTokens causes a run to split into multiple model calls, each segment is
bounded by local date/channel evidence units and the final stdout report is a chronological merge of
segment reports. The persisted memory for the run is one JSON memory object containing the extracted
memory from each segment; future runs receive that merged memory as their previous memory.
When model fallbacks are configured, segmentation uses the smallest configured
contextWindowTokens and the largest configured maxOutputTokens across the fallback chain so a
segment that fits the primary model also fits configured fallback models.
Date flags on run and collect-model-input filter the local dates included in the model
timeline. Date flags on create-config apply only to its preview and dry-run; they are not written
to the generated config.
--date YYYY-MM-DDis a shortcut for a one-day range.--date todayand--date yesterdayresolve once using the local timezone fromTZ.--window current-workday,--window previous-workday, and--window previous-5-workdaysselect named local workday ranges.--start-date YYYY-MM-DDand--end-date YYYY-MM-DDdefine an inclusive range.--start-datewithout--end-dateincludes messages from that local date forward.--end-datewithout--start-dateincludes messages up to that local date.--date,--window, and explicit--start-date/--end-dateranges are mutually exclusive.- Any date or window flag makes the run
explicit: the scan bounds come from the requested local range and prior implicit runs are ignored. - With no date/window flags, the run is
implicit: the first run starts at local today midnight, and later runs start from the latest successful implicit run'sscanEndCursor. - Implicit runs freeze
scanEndCursorbefore scanning. Completed implicit runs advance the next implicit start even when there are no matches or no model call; failed runs do not advance. - Previous memory is selected from the latest earlier successful run that actually stored memory. No-match and model-without-memory runs do not erase memory continuity, and same-range reruns do not feed on memory from an earlier run of that same range.
The global model block configures the analysis LLM used by Vercel AI SDK. The nested
context.syntheticThreads.embeddings and context.syntheticThreads.classifier blocks configure
optional helpers for synthetic-thread detection. scoredMatcherDefaults provides the same default
provider settings for scored matchers, and each scored matcher can override those defaults.
Scoring helpers also run through the AI SDK. Classifier calls use language providers such as
ollama, openai, anthropic, google, xai, openrouter, opencode, gateway, and
openai-compatible. Embedding calls require providers that expose an AI SDK embedding model, such
as ollama, openai, openrouter, google, gateway, or openai-compatible; providers without
embedding support fail with a clear error. Defaults remain local-first: classifiers use
provider: "ollama" with qwen3:0.6b, and embeddings use provider: "ollama" with
nomic-embed-text.
For local Ollama classifier setup, install Ollama and run:
pnpm run ollama:classifierThe default classifier model is qwen3:0.6b; override it with OLLAMA_CLASSIFIER_MODEL. This
optional setup script checks/starts the Ollama server, pulls the model, and sends a /api/generate
smoke request.
Local embeddings are possible with Ollama's embeddings API. The default helper model is
nomic-embed-text, which is an embeddings-only model; override it with OLLAMA_EMBEDDINGS_MODEL.
pnpm run ollama:embeddingsThe optional embeddings helper checks/starts Ollama, pulls the model, and sends a /api/embed
smoke request. Both helpers write local Ollama startup logs to tmp/ollama.log when they need to
start a server.
- Primary:
nomic-embed-text(size: 274 MB, context: 2K) - Fast path:
all-minilm(size: 46 MB, context: 512) - Heavy but multilingual and larger context:
bge-m3(size: 1.2 GB, context: 8K)
- Primary:
qwen3:0.6b(523 MB) - Alternative:
smollm2:360m(726 MB) - Heavy but safe:
llama3.2:1b(1.3 GB) - Heavy:
llama3.2:3b(2.0 GB)