#!/bin/sh # Decisive - one-line setup. # # curl -fsSL https://wemachines.com/install | sh # # Wires this repo to a Decisive workspace so your coding agent keeps the team # updated while you work. It touches four things in the current repo and nothing # else: # # .mcp.json registers Decisive's MCP server # .decisive/ WORKFLOW.md, the protocol your agent # follows; config.json, the switch that # turns it down or off; and mcp-guard.sh, # the hook that enforces both # .claude/settings.json MERGED: registers mcp-guard.sh so the # workspace write tools cannot fire from a # folder that isn't this repo # AGENTS.md (or your existing CLAUDE.md) APPENDED: a short section pointing at # WORKFLOW.md, so your agent reads it. # Claude Code reads CLAUDE.md and NOT # AGENTS.md, so when the section lands in # AGENTS.md a one-line CLAUDE.md importing # it is written too - otherwise the whole # protocol is invisible to Claude Code # # It is deliberately boring: no sudo, no PATH changes, no daemons, no telemetry, # nothing downloaded beyond this file. It never overwrites or removes anything you # wrote: new files are only created when absent, .mcp.json and .claude/settings.json # are merged key-wise, and the instructions file is appended to behind a marker. # Re-running is safe. # # Read before you pipe: https://wemachines.com/install set -eu MCP_URL="https://app.wemachines.com/api/mcp" VERSION="20" # --- output ------------------------------------------------------------------- if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); G=$(printf '\033[32m') Y=$(printf '\033[33m'); R=$(printf '\033[31m'); X=$(printf '\033[0m') else B=''; DIM=''; G=''; Y=''; R=''; X='' fi made=0 say() { printf '%s\n' "$*"; } ok() { printf ' %s+%s %s\n' "$G" "$X" "$1"; made=$((made+1)); } skip() { printf ' %s·%s %s %s(exists, left alone)%s\n' "$DIM" "$X" "$1" "$DIM" "$X"; } warn() { printf ' %s!%s %s\n' "$Y" "$X" "$1"; } die() { printf '%serror:%s %s\n' "$R" "$X" "$1" >&2; exit 1; } # --- where --------------------------------------------------------------------- if root=$(git rev-parse --show-toplevel 2>/dev/null); then cd "$root" else root=$(pwd) warn "not a git repository - installing into $root" fi say "" say "${B}Decisive${X} ${DIM}· setting up $root${X}" say "" # --- .mcp.json ----------------------------------------------------------------- # Project-scoped MCP config. Committed, so every teammate who clones the repo # gets the same connection - they each sign in as themselves on first use. write_mcp_json() { cat > .mcp.json <<-JSON { "mcpServers": { "decisive": { "type": "http", "url": "$MCP_URL" } } } JSON } # Exit codes from the two merge helpers below: 0 merged, 3 already present, # anything else = couldn't do it (missing runtime, unparseable JSON, ...). merge_with_node() { # stderr silenced: an unparseable .mcp.json would otherwise dump a raw stack # trace at the user. We want the friendly "add this block yourself" path instead. DECISIVE_MCP_URL="$MCP_URL" node 2>/dev/null <<-'NODE' const fs = require('fs'); const cfg = JSON.parse(fs.readFileSync('.mcp.json', 'utf8')); cfg.mcpServers = cfg.mcpServers || {}; if (cfg.mcpServers.decisive) process.exit(3); cfg.mcpServers.decisive = { type: 'http', url: process.env.DECISIVE_MCP_URL }; fs.writeFileSync('.mcp.json', JSON.stringify(cfg, null, 2) + '\n'); NODE } merge_with_python() { DECISIVE_MCP_URL="$MCP_URL" python3 2>/dev/null <<-'PY' import json, os, sys cfg = json.load(open('.mcp.json')) cfg.setdefault('mcpServers', {}) if 'decisive' in cfg['mcpServers']: sys.exit(3) cfg['mcpServers']['decisive'] = {'type': 'http', 'url': os.environ['DECISIVE_MCP_URL']} open('.mcp.json', 'w').write(json.dumps(cfg, indent=2, ensure_ascii=False) + '\n') PY } merge_mcp_json() { # Merge into an existing config rather than clobbering it. Node and Python are # both optional, and a present-but-broken one shouldn't strand us - try each in # turn. If neither works we print the block and let the user paste it. s=0 if command -v node >/dev/null 2>&1; then merge_with_node || s=$? if [ "$s" -eq 0 ] || [ "$s" -eq 3 ]; then return "$s"; fi fi s=0 if command -v python3 >/dev/null 2>&1; then merge_with_python || s=$? return "$s" fi return 4 } if [ ! -f .mcp.json ]; then write_mcp_json ok ".mcp.json" else status=0; merge_mcp_json || status=$? case "$status" in 0) ok ".mcp.json ${DIM}(added \"decisive\" to existing config)${X}" ;; 3) skip ".mcp.json ${DIM}already has \"decisive\"${X}" ;; *) warn ".mcp.json exists and could not be merged automatically - add:" say "" say " \"decisive\": { \"type\": \"http\", \"url\": \"$MCP_URL\" }" say "" ;; esac fi # --- .decisive/WORKFLOW.md ----------------------------------------------------- mkdir -p .decisive if [ -f .decisive/WORKFLOW.md ]; then skip ".decisive/WORKFLOW.md" else cat > .decisive/WORKFLOW.md <<-'MD' # Working with the team through Decisive This repo is connected to a Decisive workspace over MCP - the team's shared tasks, discussions, library pages and chat. Treat it as the team's memory: someone will read what you leave behind, possibly weeks from now, possibly after you've forgotten this session existed. The scope rules (when the write tools may be used at all), the `auto_mode` switch in `.decisive/config.json`, and the `start_work` / `end_work` contract are stated in the MCP server's own instructions and enforced by `.decisive/mcp-guard.sh`. This file covers what those don't. You do not report progress by hand. Every write tool re-stamps your live session as it goes, so staying visible is a side effect of doing the work. ## Before you start `get_workspace` to confirm where you are, then `list_tasks` to see what's in flight - the thing being asked for is often already a task, sometimes already assigned to someone. If `get_workspace` answers with a slug other than the `workspace` in `.decisive/config.json`, **stop and say so**: writing into the wrong workspace is worse than writing nothing. If the request is vague, `list_discussions` and `list_recent_chat_messages` usually explain what the team has already decided and why. `read_task` gives the full body and comments - the comments hold the corrections. ## When someone is already there `read_task` returns `live_work` and `start_work` answers with a `conflict` when another member's AI is on that task. That is not a refusal - two people on one ticket is fine - but **stop and say so before you continue**, and if you both carry on, say which part you're taking so you don't collide. No task for what you're doing? Pass `title` to `start_work` and one is created for you, already in progress and assigned to you. Work nobody can see is worse than a slightly busier board. `create_task` puts the task on your list by default; pass `assignee_ids` when it is genuinely someone else's, or `[]` for backlog nobody has picked up. ## What deserves a write **Do not comment on a ticket unless you were asked to.** This is the rule people actually notice you breaking. A comment notifies teammates and sits on the ticket forever, so it is never the place to say you started, finished, or changed something - your live session already shows what you are on, and the commit already says what you did. A comment that repeats either is noise someone has to read past. Opening a ticket is different, and welcome: `create_task` for a real bug you found outside your scope, or for work that deserves its own line on the board. The narrow exception is being genuinely stuck. If a decision only a human can make is blocking you, say so to the person you're working with - a comment only when there is no such person (an autonomous run), and then one comment with the actual options, not a status update. Decisions and rationale belong in a discussion, not a chat message that scrolls away: `create_discussion` with what was decided and why the alternatives lost. Chat is for the moment, discussions are for the record. `send_chat_message` only when the team needs to know now - a deploy, a migration, a breaking change, a decision that blocks someone else's work. ## Commits Reference the task in every commit that contributes to one - the identifier at the end of the subject line (`fix login redirect (T-42)`), or a `Task: T-42` trailer if the repo has its own subject conventions. Teammates and the workspace AI read the repo's history: a referenced commit ties the code back to the task that caused it; an unreferenced one breaks that trail. No task for the work? `create_task` first - anything more than a quick fix deserves one. Then move what you just committed. A commit is the moment the board goes stale - the code changed and the column did not - and nobody comes back to tidy it later. Call `update_task_status` for every task the commit touched: the column your workspace uses for "shipped, not yet verified" when the commit finishes the work, the in-progress column when it is only a step on the way. Name a status that does not exist and the tool answers with the ones that do. Never mark a task done - a human verifies the work and closes it, and the tool enforces that - unless the repo opts in: `status_after_push` in `.decisive/config.json` names the column finished work moves to once it is pushed (for example "Done"), and passing `status_after_push: true` on `update_task_status` or `end_work` is how you use that opt-in. Never pass the flag in a repo whose config does not set the key; cancelled-like statuses are refused either way. ## Writing it well Bodies take **markdown, and it renders**: headings, tables, lists, code blocks, quotes and links become real formatting, so give a body structure instead of one flat paragraph. Chat messages and comments render the same set minus headings and tables. - A task: title it imperative and specific - "Cut onboarding to three steps", not "Onboarding" - then keep the body SHORT. A few sentences on what needs doing and how anyone will know it's done. Most tasks need no headings at all; `## Scope` / `## Done when` are for work genuinely big enough to have sections, and a three-heading document for a one-line fix is noise the assignee reads past. Never pad a body to look thorough. - A discussion: the decision, the constraints it has to fit, and what would make an option win, with the real alternatives spelled out - those are what the team picks between when it seals the decision. - Refer to tasks, discussions, pages and apps by code (T-7, D-1, P-3, A-app-slug) and they render as live links carrying the item's current title. Use a table whenever you are comparing things. - Real content only: no placeholders, no invented numbers or names. If you do not know something, say so. The bar for anything you write: would a human on this team have bothered to write it? If not, don't. A workspace full of bot chatter is worse than an empty one, because people stop reading it. ## Asking how Decisive works `ask_support` answers questions about Decisive itself - what it is, how each section works, the AI and credits model. Use it instead of guessing at the platform's behaviour. It comes in sections: call it with no arguments to see the list, then ask for the one you need. Only `section: 'all'` returns the whole ~8,000-token block, and whatever it returns stays in your context for the rest of the session. MD ok ".decisive/WORKFLOW.md" fi # --- .decisive/config.json ----------------------------------------------------- # The switch. WORKFLOW.md tells the agent to announce its work; this is how you turn # that down without disconnecting. Committed, so it is a team decision rather than a # per-laptop one - and `auto_mode: ask` is the middle setting people actually want. # Optional key, not written here: `status_after_push` names the column finished work # moves to once it is pushed (e.g. "Done") - without it, completed statuses stay a # human-only call. if [ -f .decisive/config.json ]; then skip ".decisive/config.json" else cat > .decisive/config.json <<-'JSON' { "auto_mode": "on" } JSON ok ".decisive/config.json ${DIM}(auto_mode: on | ask | off · optional status_after_push)${X}" fi # --- .decisive/mcp-guard.sh ---------------------------------------------------- # Enforcement for the above. The MCP server tells every client to call start_work # before it changes anything - correct in this repo, wrong in every other folder a # globally-registered client follows you into. This blocks the write tools when the # working directory is not inside a Decisive repo, and honours auto_mode when it is. # # NOT a <<- heredoc: that strips every leading tab, which would flatten the script's # own indentation. Content sits at column 0 and ships verbatim. if [ -f .decisive/mcp-guard.sh ]; then skip ".decisive/mcp-guard.sh" else cat > .decisive/mcp-guard.sh << 'GUARD' #!/bin/sh # Decisive - MCP auto-mode guard. A Claude Code PreToolUse hook on mcp__decisive__.* # # The MCP server tells every client to call start_work before it changes anything. That # is right inside the repo the workspace is linked to, and wrong everywhere else: register # the server globally (`claude mcp add -s user`, Cursor's global mcp.json, Claude Desktop) # and the same instruction rides along into every folder you open - so a private # conversation in ~/finances opens a task on a shared team board under your name. # # The server's instructions say so too, but a prompt is advisory. This is the enforcement: # # * WRITE tools are blocked unless the working directory sits inside a tree that has a # `.decisive/` directory - i.e. a repo somebody actually ran the installer in. # * Inside such a repo, `.decisive/config.json` decides: # "auto_mode": "on" allow (default when the file or key is absent) # "auto_mode": "ask" prompt first # "auto_mode": "off" block # DECISIVE_AUTO_MODE in the environment overrides the file, for a one-off session: # DECISIVE_AUTO_MODE=off claude # * READ tools are never blocked. The leak direction is outward - reading the workspace # from an unrelated folder publishes nothing to anyone. # # What it deliberately does NOT do: judge whether the conversation is on-topic. A hook # cannot read intent, so "only when the context is related to the workspace" stays with # the model (the SCOPE paragraph in the server's instructions and in WORKFLOW.md). This # guard covers the part that is mechanically decidable: WHERE you are running. # # Install it two ways, and prefer both: # # in this repo .claude/settings.json - enforces auto_mode for everyone who clones it # for every repo ~/.claude/hooks/decisive-mcp-guard.sh + the same block in # ~/.claude/settings.json - the only one that covers folders that are # NOT Decisive repos, which is where the leak actually happens # # {"hooks":{"PreToolUse":[{"matcher":"mcp__decisive__.*","hooks":[ # {"type":"command","command":"sh ~/.claude/hooks/decisive-mcp-guard.sh"}]}]}} # # Nothing here reads $CLAUDE_PROJECT_DIR, so the script behaves identically from either # location - it takes the directory from the hook payload's `cwd`. # # POSIX sh, no jq: this ships to other people's machines through the installer, and a # guard that fails open on a missing dependency is not a guard. set -u payload=$(cat) # One field out of the hook payload. Values here (`tool_name`, `cwd`) are a tool name and a # filesystem path - no embedded quotes or escapes to worry about in practice. field() { printf '%s' "$payload" | sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1 } tool=$(field tool_name) # Not ours -> say nothing. Silence is "allow" for a PreToolUse hook. case "$tool" in mcp__decisive__*) ;; *) exit 0 ;; esac # Tools that publish to the workspace. PARITY: mirrors `write: true` in the tool catalog # (functions/utils/mcp-tools.ts) - a new write tool must be added here in the same session, # or it ships ungated. Anything absent from this list is treated as a read and allowed. # # Deliberately NOT here: the `mcp_*` tools a connection picks up when it is authorized as # one of the workspace's agents. They are marked `write` on the server so a read-only token # cannot reach them, but they call someone else's system - Linear, PostHog - and publish # nothing to the team's board, which is the only thing this guard exists to keep out of # unrelated folders. `load_skill` is absent for the same reason: it reads. WRITE_TOOLS=" start_work end_work create_task update_task_status add_comment create_discussion create_page send_chat_message run_coding_agent propose_skill create_calendar_event update_calendar_event cancel_calendar_event " name=${tool#mcp__decisive__} case "$WRITE_TOOLS" in *" $name "*) ;; *) exit 0 ;; esac # Reasons are plain ASCII with no quotes or backslashes, so they need no JSON escaping. decide() { printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s","permissionDecisionReason":"%s"}}\n' "$1" "$2" exit 0 } # --- where are we? ------------------------------------------------------------- # Walk up for the `.decisive/` marker the installer leaves. `cwd` comes from the payload # rather than $PWD because a hook runs in the harness's directory, not the session's. cwd=$(field cwd) [ -n "$cwd" ] || cwd=$PWD root='' dir=$cwd while [ -n "$dir" ] && [ "$dir" != '/' ]; do if [ -d "$dir/.decisive" ]; then root=$dir break fi dir=$(dirname "$dir") done if [ -z "$root" ]; then decide deny "$cwd is not a Decisive-connected repo (no .decisive directory above it). Decisive's write tools post to a shared team board, so they are blocked outside the linked repo - this keeps unrelated work off your team's board. Read tools still work. To connect this repo, run: curl -fsSL https://wemachines.com/install | sh" fi # --- is auto-mode on? ---------------------------------------------------------- mode=${DECISIVE_AUTO_MODE:-} if [ -z "$mode" ] && [ -f "$root/.decisive/config.json" ]; then mode=$(sed -n 's/.*"auto_mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$root/.decisive/config.json" | head -n 1) fi [ -n "$mode" ] || mode=on case "$mode" in off) decide deny "Decisive auto-mode is off (auto_mode in $root/.decisive/config.json, or DECISIVE_AUTO_MODE). $name would write to the team workspace. Set auto_mode to on to restore it, or ask the user to run this action explicitly." ;; ask) decide ask "Decisive auto-mode is set to ask. $name writes to the team workspace - confirm before it posts." ;; esac exit 0 GUARD chmod +x .decisive/mcp-guard.sh ok ".decisive/mcp-guard.sh ${DIM}(blocks workspace writes from other folders)${X}" fi # --- .claude/settings.json ----------------------------------------------------- # Registers the guard for Claude Code. Merged the same way .mcp.json is, because this is # a file the user maintains. Other clients read the WORKFLOW.md rules but have no hook # system to enforce them - that gap is why the consent screen also offers read-only. CLAUDE_HOOK_CMD='sh "$CLAUDE_PROJECT_DIR/.decisive/mcp-guard.sh"' merge_settings_with_node() { DECISIVE_HOOK_CMD="$CLAUDE_HOOK_CMD" node 2>/dev/null <<-'NODE' const fs = require('fs'); const p = '.claude/settings.json'; const cfg = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {}; cfg.hooks = cfg.hooks || {}; cfg.hooks.PreToolUse = cfg.hooks.PreToolUse || []; const has = JSON.stringify(cfg.hooks.PreToolUse).includes('mcp-guard.sh'); if (has) process.exit(3); cfg.hooks.PreToolUse.push({ matcher: 'mcp__decisive__.*', hooks: [{ type: 'command', command: process.env.DECISIVE_HOOK_CMD, timeout: 10 }], }); fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n'); NODE } merge_settings_with_python() { DECISIVE_HOOK_CMD="$CLAUDE_HOOK_CMD" python3 2>/dev/null <<-'PY' import json, os, sys p = '.claude/settings.json' cfg = json.load(open(p)) if os.path.exists(p) else {} cfg.setdefault('hooks', {}).setdefault('PreToolUse', []) if 'mcp-guard.sh' in json.dumps(cfg['hooks']['PreToolUse']): sys.exit(3) cfg['hooks']['PreToolUse'].append({ 'matcher': 'mcp__decisive__.*', 'hooks': [{'type': 'command', 'command': os.environ['DECISIVE_HOOK_CMD'], 'timeout': 10}], }) open(p, 'w').write(json.dumps(cfg, indent=2, ensure_ascii=False) + '\n') PY } merge_settings() { s=0 if command -v node >/dev/null 2>&1; then merge_settings_with_node || s=$? if [ "$s" -eq 0 ] || [ "$s" -eq 3 ]; then return "$s"; fi fi s=0 if command -v python3 >/dev/null 2>&1; then merge_settings_with_python || s=$? return "$s" fi return 4 } mkdir -p .claude status=0; merge_settings || status=$? case "$status" in 0) ok ".claude/settings.json ${DIM}(registered the guard hook)${X}" ;; 3) skip ".claude/settings.json ${DIM}already registers the guard${X}" ;; *) warn ".claude/settings.json could not be merged automatically - add under \"hooks\":" say "" say " \"PreToolUse\": [{ \"matcher\": \"mcp__decisive__.*\", \"hooks\": [" say " { \"type\": \"command\", \"command\": \"$CLAUDE_HOOK_CMD\" } ]}]" say "" ;; esac # --- pointer in the agent instructions file ------------------------------------ # Appended to whichever instruction file the repo already uses, so it's picked up # without us bloating a file the user maintains. Marker keeps re-runs idempotent. MARKER="" if [ -f CLAUDE.md ]; then TARGET=CLAUDE.md elif [ -f AGENTS.md ]; then TARGET=AGENTS.md else TARGET=AGENTS.md; : > "$TARGET" fi # A CLAUDE.md that imports AGENTS.md (the bridge below writes exactly that) already # carries the section from there, so the marker lives in the OTHER file. Without this # the second run reads an unmarked CLAUDE.md and states the whole protocol twice. bridged=0 if [ "$TARGET" = CLAUDE.md ] && grep -qE '^@AGENTS\.md[[:space:]]*$' CLAUDE.md 2>/dev/null && grep -qF "$MARKER" AGENTS.md 2>/dev/null; then bridged=1 fi if grep -qF "$MARKER" "$TARGET" 2>/dev/null || [ "$bridged" -eq 1 ]; then skip "$TARGET ${DIM}already references the workflow${X}" else [ -s "$TARGET" ] && printf '\n' >> "$TARGET" cat >> "$TARGET" <<-MD $MARKER ## Decisive This repo is connected to a Decisive workspace over MCP, so the team can see your work as it happens. Read \`.decisive/WORKFLOW.md\` and follow it - and whatever else you do, these three: - \`start_work\` BEFORE you touch anything - the moment you're asked to build, fix or change something. Pass \`title\` if the work has no task yet. - reference the task in every commit that contributes to it - \`(T-42)\` on the subject line, or a \`Task: T-42\` trailer - then \`update_task_status\` to move what you shipped. - \`end_work\` when you stop, with what actually changed. You never report progress by hand - every write tool keeps your live session alive. That applies to work **on this repo's project**. The write tools post to a shared team board, so anything personal, general or off-project stays off it - answer it and write nothing, and never copy conversation text or file contents into a summary or comment. \`.decisive/config.json\` sets \`auto_mode\`: \`ask\` means confirm first, \`off\` means only when you're asked for that action by name. MD ok "$TARGET ${DIM}(appended Decisive section)${X}" fi # --- CLAUDE.md bridge ---------------------------------------------------------- # Claude Code reads CLAUDE.md and does NOT read AGENTS.md, so a section that landed # in AGENTS.md above never enters its context - the agent connects, gets the tools, # and still never calls start_work, because nothing ever told it to. Anthropic's # documented fix is a CLAUDE.md that imports the other file, which is one line and # leaves AGENTS.md the single place the protocol is written. Deliberately outside the # marker check above: a repo installed before v12 has the section in AGENTS.md already, # and re-running is what gives it the bridge. if [ "$TARGET" = AGENTS.md ] && [ ! -f CLAUDE.md ]; then printf '@AGENTS.md\n' > CLAUDE.md ok "CLAUDE.md ${DIM}(imports AGENTS.md - Claude Code doesn't read AGENTS.md)${X}" fi # --- next steps ---------------------------------------------------------------- say "" if [ "$made" -eq 0 ]; then say "${B}Already set up.${X} Nothing to change." else say "${B}Done.${X} Commit these so your team gets them too." fi say "" say "${B}Next${X}" say " 1. Open this repo in Claude Code, Cursor, Codex or any MCP client." say " 2. It will ask you to sign in to Decisive and pick a workspace - and, if you" say " like, one of your agents to run as." say " ${DIM}No account yet? https://wemachines.com${X}" say " 3. Ask it: ${DIM}\"what's on my plate?\"${X}" say "" say " ${DIM}MCP setup: https://app.wemachines.com -> MCP · installer v$VERSION${X}" say ""