Skill: ldodee/project-os-organizer (version 1.2.1)

--- name: project-os-organizer description: "Privacy-first, chat-first project manager for vibe coders. Track projects, capture updates, and resume work across local folders, Claude/Codex, and GitHub with explicit opt-in controls." metadata: {"clawdbot":{"emoji":"🗂️","requires":{"bins":["python3"],"env":["PROJECT_OS_ROOT (required unless explicit remote install opt-in)","PROJECT_OS_INCLUDE_CHAT_ROOTS=1 (optional)","PROJECT_OS_ENABLE_GITHUB_SYNC=1 (optional)"]},"os":["linux","darwin","win32"]}} --- # Project OS Organizer Skill ## Why This Skill Use this when a user is juggling many AI-built projects and needs one simple command surface to: 1. See what is active now. 2. Capture progress and next steps fast. 3. Resume any project in one jump (local/GitHub/chat). ## Security Defaults 1. Remote install is disabled by default. 2. Chat transcript indexing is disabled by default. 3. GitHub sync/token usage is disabled by default. 4. Home-directory heuristic discovery is disabled by default. To opt in explicitly: 1. `PROJECT_OS_INCLUDE_CHAT_ROOTS=1` enables chat transcript indexing. 2. `PROJECT_OS_ENABLE_GITHUB_SYNC=1` enables GitHub username/token integration. 3. [REDACTED_SECRET] enables broad home-directory discovery. 4. `PROJECT_OS_AUTO_SETUP=1 PROJECT_OS_ALLOW_REMOTE_INSTALL=1` allows remote clone/install if `PROJECT_OS_ROOT` is missing. ## Goal Produce a complete, local-first project inventory that answers: 1. What projects exist right now? 2. Which are active/blocked/stale? 3. What is each project actually about? 4. Where did the user leave off? 5. How can the user jump back in immediately? 6. How can the user quickly edit status/next steps/items without leaving OpenClaw? ## Default Behavior (Natural Language First) Default action for every user message: 1. Interpret the message. 2. Run: `scripts/project_router.sh "<user message>"`. 3. Return the router output directly in plain language. Users should not need to type scripts, Python, paths, or flags. Fallback for power users: - Use the short wrapper command: `project ...` - Examples: `project`, `project focus`, `project today`, `project inbox "idea: test"`, `project dashboard start` Plain-language request mapping: 1. "What am I working on today?" -> activity today 2. "Show my projects" -> grouped project list (Now/Later/Blocked/Done) 3. "Focus list" -> top focus items 4. "Add this idea: ..." -> inbox capture 5. "Next for project-os: ..." -> add next 6. "Mark X blocked" -> set simple status 7. "Start dashboard" -> dashboard start 8. "Resume project X" -> resume pack 9. "Only track A, B, C" -> set tracked scope (activity will only include these) 10. "Mute X" / "Untrack X" / "Show scope" -> scope controls Response style in non-technical mode: 1. Keep responses short and direct. 2. Do not expose technical command details unless user asks. 3. For activity questions, always return `Activity Criteria` + day sections. 4. If a project name is ambiguous, ask one short clarification question. ## Project Definition Treat an item as a project if it matches any of these: 1. Git repository folder. 2. Non-git code folder with project markers (`package.json`, `pyproject.toml`, `Cargo.toml`, etc). 3. Chat session (Claude/Codex) that does not match an existing project; create a chat-derived project. 4. Claude workspace chats should map to deeper subprojects when session folders indicate a nested workspace path. Reject likely non-project folders in collection roots (docs-only, logs, backups, notes-only folders) unless there is strong code/marker evidence. Reject low-signal chat-only entries as standalone projects unless they contain a useful path hint or meaningful project intent. ## Required Workflow 1. Run `scripts/bootstrap.sh` first. 3. Validate coverage: - run `python3 -m project_os.cli --db ~/.project_os/openclaw_test.db --config ~/.project_os/openclaw_test_config.json list-projects --limit 200` - run `python3 -m project_os.cli --db ~/.project_os/openclaw_test.db --config ~/.project_os/openclaw_test_config.json list-sessions --limit 50` - run `python3 -m project_os.cli --db ~/.project_os/openclaw_test.db --config ~/.project_os/openclaw_test_config.json list-items --status open --limit 80` - run `python3 -m project_os.cli --db ~/.project_os/openclaw_test.db --config ~/.project_os/openclaw_test_config.json squash-chat-projects` 4. If user wants visual mode, run `scripts/start_dashboard.sh`. 5. If user wants quick resume context, run `scripts/write_memory.sh` and use `~/.project_os/PROJECT_MEMORY.md`. ## First-Run (Production) Run this for a first-time user: 1. `scripts/openclaw_smoke_agent.sh` 2. `scripts/read_smoke_result.sh` 3. Confirm: - `RESULT_STATUS=ok` - `RESULT_DASHBOARD_URL=...` - `RESULT_PORT_8765_LISTENING=yes` If `project-os` repo is not present locally, the skill will fail safely by default and instruct setting `PROJECT_OS_ROOT`. Remote install is explicit opt-in only. ## Operation Modes - Dashboard mode: simple visual triage (`Focus Today`, `Now`, `Blocked`, `Later`, `Done`) and jump links. - Dashboard mode includes: - Quick Inbox capture (freeform note -> auto-routed update/next/reminder/idea/blocker) - Daily Check-In panel (done + next + blocker) - Focus list, stale nudges, weekly snapshot, and server health - Dashboard intentionally avoids recommendation-score noise and focuses on plain language: - `What it is` (derived from project files like README/package metadata) - `Where left off` - `Do next` - inline editing: set simple status (`now/later/blocked/done`), set next action, add update/next/reminder/idea/blocker, mark items done/dismissed - session resume buttons for both tools (`Resume in Claude` and `Resume in Codex`) when session link is valid - Memory mode: markdown snapshot for fast resume in any chat. Default recommendation: use both. ## OpenClaw Command-First Updates Use CLI commands as the primary interface: 1. `add-update` for what changed. 2. `add-next` for the immediate next step (also updates project `next_action`). 3. `add-reminder` for date/time-based follow-ups. 4. `add-idea` for backlog thoughts. 5. `list-items` and `set-item-status` to triage/close items. 6. Use `scripts/project_actions.sh` for short aliases around these commands. 7. For activity-window questions ("what did I work on today/yesterday"), run `scripts/activity_report.sh --when both` or `scripts/project_actions.sh activity --when both`. 8. Do not guess activity by project name or status; only use timestamp evidence from the report criteria. 9. Activity report excludes archived projects by default (use `--include-archived` only if user asks). 10. Response contract for activity-window answers: - always include `Activity Criteria` - always include `Today (...)` and/or `Yesterday (...)` headings - include at least one evidence line per listed project (`local_commit`, `github_push`, `session`, or `note`) - if no projects match, explicitly return `- none` ## OpenClaw Usage 1. In OpenClaw, invoke this skill by name: `project-os-organizer`. 2. For first run setup: `project setup` (or `scripts/easy_mode.sh setup`). 3. Default day-to-day: just type plain English in chat and route through `project_router.sh`. 4. Optional shortcuts use only `project ...`. 5. For one-command local install from this repo, run `scripts/install_openclaw_skill.sh`. 6. For non-interactive OpenClaw agents, use: - `scripts/openclaw_smoke.sh` for strict CI-style smoke (non-zero exit on failure) - `scripts/openclaw_smoke_agent.sh` for agent-safe smoke (always exits zero, returns `RESULT_STATUS`) - `scripts/read_smoke_result.sh` to fetch the last smoke result file when command output is flaky - `scripts/bootstrap.sh --noninteractive` (timeout-safe quick mode) - `scripts/bootstrap.sh --noninteractive-full` (full refresh mode) - `scripts/start_dashboard.sh --detach --restart` - Smoke commands return machine-readable lines: - `RESULT_STATUS` - `RESULT_ERROR` - `RESULT_DASHBOARD_URL` - `RESULT_DASHBOARD_PID` - `RESULT_PORT_8765_LISTENING` - `RESULT_CURL_HEAD` - `RESULT_SMOKE_LOG` - `RESULT_DASHBOARD_LOG` - `RESULT_RESULT_FILE` 7. For quick editing from chat: - `scripts/project_actions.sh list --limit 80` - `scripts/project_actions.sh set-status --project "<name|id>" --status blocked` - `scripts/project_actions.sh set-next --project "<name|id>" --text "next step"` - `scripts/project_actions.sh add-update --project "<name|id>" --text "what changed"` - `scripts/project_actions.sh add-next --project "<name|id>" --text "immediate next step"` - `scripts/project_actions.sh add-reminder --project "<name|id>" --text "follow up" --due 2026-03-01` - `scripts/project_actions.sh add-blocker --project "<name|id>" --text "what is blocked"` - `scripts/project_actions.sh simple-status --project "<name|id>" --status now` - `scripts/project_actions.sh focus --limit 3` - `scripts/project_actions.sh stale --days 14 --limit 20` - `scripts/project_actions.sh weekly --days 7 --limit 12` - `scripts/project_actions.sh notify --period daily` - `scripts/project_actions.sh inbox --text "freeform note"` - `scripts/project_actions.sh checkin --project "<name|id>" --done "..." --next "..." --blocker "..."` - `scripts/project_actions.sh duplicates --limit 50` - `scripts/project_actions.sh merge --keep "<id|name>" --drop "<id|name>"` - `scripts/project_actions.sh ask --text "mark project-os blocked"` - `scripts/project_actions.sh set-item --item <id> --status done` 8. For "worked on today/yesterday": - `scripts/project_actions.sh activity --when both` 9. For personal scope control: - `scripts/project_actions.sh scope --set "project-os" "polymarket-trader-v2"` - `scripts/project_actions.sh track --project "project-os"` - `scripts/project_actions.sh mute --project "clawd"` - `scripts/project_actions.sh scope` - Add `--include-archived` only on explicit request. - Return the output sections `Activity Criteria`, `Today (...)`, and `Yesterday (...)` so the user sees exactly how projects were counted. ## Safety and Scope 1. Local-first only. 2. Do not mutate repositories/servers automatically. 3. Do not invent project state; infer from scans/sessions and expose uncertainty. 4. Prefer broad root coverage to avoid missing nested subfolders. 5. Keep non-git project discovery at root-level by default (`include_nested_non_git_projects: false`) to avoid noisy submodules. 6. Keep collection discovery strict: include real project subfolders, drop random text-only folders. ## References - `references/project-definition.md` - `references/workflow.md`

path: USER_QUICKSTART.md
content: # Project Organizer Quickstart

## 1) Start the skill
In OpenClaw, use this exact skill:

`project-os-organizer`

## 2) Just type normal phrases
Examples:
- "Show my projects"
- "What am I working on today?"
- "Focus list"
- "Add this idea: improve onboarding flow"
- "Next for project-os: run smoke tests"
- "Mark project-os blocked"
- "Start dashboard"
- "Only track project-os, polymarket-trader-v2, poly_market_edge_finder"
- "Show scope"

## 3) Optional shortcuts (power users)
Use the short `project` command:

```bash
project
project focus
project today
project inbox "idea: improve onboarding flow"
project next "project-os" "run smoke tests"
project status "project-os" blocked
project scope set "project-os" "polymarket-trader-v2" "poly_market_edge_finder"
project scope
project weekly
project blockers
project dashboard start
```

## 4) Dashboard
Start:

```bash
project dashboard start
```

Open:

`http://127.0.0.1:8765`

---
Troubleshooting (if needed): run `project setup` once, then retry.

Privacy/Security defaults:
- Chat indexing is OFF by default.
- GitHub sync is OFF by default.
- Remote auto-install is OFF by default.

Optional opt-ins:
- `export PROJECT_OS_INCLUDE_CHAT_ROOTS=1`
- `export PROJECT_OS_ENABLE_GITHUB_SYNC=1`
- `export PROJECT_OS_AUTO_SETUP=1 PROJECT_OS_ALLOW_REMOTE_INSTALL=1`


path: references/project-definition.md
content: # Project Definition

A project is anything the user may need to resume and ship.

## Inclusion Rules
1. Git repo roots.
2. Nested project folders with code markers even without `.git`.
3. Chat-derived projects from Claude/Codex sessions when no existing project match exists.
4. Collection subfolders only when they have strong project signals (markers/code/hint dirs), not docs-only noise.
5. For Claude chats, infer nested workspace project from Claude session folder segment when direct hint is too generic.
6. Do not promote low-signal chat transcripts to projects (UUID-like title + generic snippet + no useful hint).

## Project Description Rules
1. Prefer manifest descriptions (`package.json`, `pyproject.toml`, `Cargo.toml`).
2. Else use first meaningful README paragraph.
3. Else infer concise type from stack (`Next.js`, Python, Go, Rust, etc).
4. For chat-only projects, prefer hint-path label (for example workspace subfolder name) over UUID session titles.
5. Else fallback to short chat/title-based summary.

## Currentness Signals
1. Latest commit time/message.
2. Latest GitHub push.
3. Latest chat session activity.
4. Explicit status and open next-step items.

## Left-Off Summary Priority
1. TODO-like signals.
2. Latest commit message.
3. Latest chat session title/snippet.
4. Fallback: simple plain-language phrase (`Working on <project>`).


path: references/workflow.md
content: # Workflow

## Setup
1. Create config and db paths dedicated to OpenClaw test mode.
2. Include broad local roots and chat roots.

## Refresh
1. Scan local roots recursively.
2. Include non-git project marker folders and strict collection subfolder discovery.
3. Sync GitHub when token/user available.
4. Index sessions and promote unmatched sessions into chat projects.
5. Regenerate summaries and recommendations.
6. Derive `What it is` project descriptions from local project files where possible.
7. Write memory snapshot.

## Resume
1. Open dashboard for visual context and jump links.
2. Use `View Chat` and both resume actions (`Resume in Claude` and `Resume in Codex`) to jump into active sessions (only when session link is valid).
3. Keep dashboard language simple: check `What it is`, `Where left off`, and `Do next`.
4. Use memory markdown for quick command-driven resume.


path: scripts/activity_report.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

project_os_cli activity-report "$@"


path: scripts/bootstrap.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

start_dashboard=0
compact=0
skip_status=0
detach_dashboard=0
quick_setup=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --dashboard)
      start_dashboard=1
      shift
      ;;
    --dashboard-detach)
      start_dashboard=1
      detach_dashboard=1
      shift
      ;;
    --compact)
      compact=1
      shift
      ;;
    --no-status)
      skip_status=1
      shift
      ;;
    --noninteractive)
      compact=1
      skip_status=1
      detach_dashboard=1
      quick_setup=1
      shift
      ;;
    --noninteractive-full)
      compact=1
      skip_status=1
      detach_dashboard=1
      quick_setup=0
      shift
      ;;
    --quick)
      quick_setup=1
      shift
      ;;
    --help|-h)
      cat <<'USAGE'
Usage: bootstrap.sh [--dashboard] [--dashboard-detach] [--compact] [--no-status] [--quick] [--noninteractive] [--noninteractive-full]

Runs first-time OpenClaw setup for Project OS:
1) write/update test config
2) init db
3) refresh projects/sessions
4) write memory snapshot
5) print quick status

Options:
  --dashboard         Start dashboard in foreground
  --dashboard-detach  Start dashboard in background
  --compact           Reduce verbose output
  --no-status         Skip quick status listing
  --quick             Skip refresh; only ensure config/db + memory
  --noninteractive    Compact + no-status + detached dashboard + quick mode (timeout-safe)
  --noninteractive-full
                      Compact + no-status + detached dashboard with full refresh
USAGE
      exit 0
      ;;
    *)
      printf 'Unknown option: %s\n' "$1" >&2
      exit 1
      ;;
  esac
done

print_runtime_config
if [[ "$quick_setup" -eq 1 ]]; then
  "$SCRIPT_DIR/setup_test_config.py"
  project_os_cli init
else
  if [[ "$compact" -eq 1 ]]; then
    PROJECT_OS_COMPACT=1 "$SCRIPT_DIR/run_refresh.sh" --compact
  else
    "$SCRIPT_DIR/run_refresh.sh"
  fi
fi
"$SCRIPT_DIR/write_memory.sh"
if [[ "$skip_status" -eq 0 ]]; then
  "$SCRIPT_DIR/quick_status.sh"
fi

if [[ "$start_dashboard" -eq 1 ]]; then
  printf 'Starting dashboard at http://%s:%s\n' "$PROJECT_OS_HOST" "$PROJECT_OS_PORT"
  if [[ "$detach_dashboard" -eq 1 ]]; then
    "$SCRIPT_DIR/start_dashboard.sh" --detach --restart
  else
    exec "$SCRIPT_DIR/start_dashboard.sh"
  fi
fi

if [[ "$quick_setup" -eq 1 ]]; then
  printf 'OpenClaw quick setup complete. Run bootstrap.sh --noninteractive-full for full refresh.\n'
else
  printf 'OpenClaw setup complete. Run start_dashboard.sh when you want the UI.\n'
fi


path: scripts/common.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ENV_FILE="$SKILL_DIR/.project_os_env"

if [[ -f "$ENV_FILE" ]]; then
  # shellcheck disable=SC1090
  source "$ENV_FILE"
fi

PROJECT_OS_DB="${PROJECT_OS_DB:-$HOME/.project_os/openclaw_test.db}"
PROJECT_OS_CONFIG="${PROJECT_OS_CONFIG:-$HOME/.project_os/openclaw_test_config.json}"
PROJECT_OS_MEMORY="${PROJECT_OS_MEMORY:-$HOME/.project_os/PROJECT_MEMORY.md}"
PROJECT_OS_HOST="${PROJECT_OS_HOST:-127.0.0.1}"
PROJECT_OS_PORT="${PROJECT_OS_PORT:-8765}"
PROJECT_OS_AUTO_SETUP="${PROJECT_OS_AUTO_SETUP:-0}"
PROJECT_OS_ALLOW_REMOTE_INSTALL="${PROJECT_OS_ALLOW_REMOTE_INSTALL:-0}"
PROJECT_OS_REPO_URL="${PROJECT_OS_REPO_URL:-https://github.com/LDodee/project-os.git}"
PROJECT_OS_INSTALL_DIR="${PROJECT_OS_INSTALL_DIR:-$HOME/.project_os/project-os}"
PROJECT_OS_TRUSTED_REPO_URL="${PROJECT_OS_TRUSTED_REPO_URL:-https://github.com/LDodee/project-os.git}"

export PROJECT_OS_DB
export PROJECT_OS_CONFIG
export PROJECT_OS_MEMORY
export PROJECT_OS_HOST
export PROJECT_OS_PORT
export PROJECT_OS_AUTO_SETUP
export PROJECT_OS_ALLOW_REMOTE_INSTALL
export PROJECT_OS_REPO_URL
export PROJECT_OS_INSTALL_DIR
export PROJECT_OS_TRUSTED_REPO_URL
export PROJECT_OS_PRIVACY_MODE
export PROJECT_OS_ENABLE_HOME_DISCOVERY
export PROJECT_OS_INCLUDE_CHAT_ROOTS
export PROJECT_OS_ENABLE_GITHUB_SYNC
export PROJECT_OS_EXTRA_ROOTS

_is_project_os_root() {
  local candidate="$1"
  [[ -n "$candidate" ]] || return 1
  [[ -d "$candidate" ]] || return 1
  [[ -f "$candidate/pyproject.toml" ]] || return 1
  [[ -f "$candidate/project_os/cli.py" ]] || return 1
}

_discover_project_os_root() {
  local maybe_repo_root=""
  maybe_repo_root="$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd || true)"

  local candidates=(
    "${PROJECT_OS_ROOT:-}"
    "$PWD"
    "$maybe_repo_root"
    "$HOME/GitHub/04-products/project-os"
    "$HOME/GitHub/project-os"
    "$HOME/project-os"
  )

  local candidate
  for candidate in "${candidates[@]}"; do
    if _is_project_os_root "$candidate"; then
      printf '%s\n' "$candidate"
      return 0
    fi
  done

  return 1
}

_install_project_os_root() {
  if [[ "$PROJECT_OS_AUTO_SETUP" != "1" || "$PROJECT_OS_ALLOW_REMOTE_INSTALL" != "1" ]]; then
    return 1
  fi
  if ! command -v git >/dev/null 2>&1; then
    printf 'Could not locate project-os and git is unavailable for remote install.\n' >&2
    return 1
  fi
  if [[ "$PROJECT_OS_REPO_URL" != "$PROJECT_OS_TRUSTED_REPO_URL" ]]; then
    printf 'Remote install blocked: PROJECT_OS_REPO_URL must match PROJECT_OS_TRUSTED_REPO_URL.\n' >&2
    return 1
  fi

  mkdir -p "$(dirname "$PROJECT_OS_INSTALL_DIR")"
  if [[ -d "$PROJECT_OS_INSTALL_DIR/.git" ]]; then
    # Existing clone is reused as-is; no forced reset/update.
    :
  elif [[ -d "$PROJECT_OS_INSTALL_DIR" ]]; then
    printf 'Remote install blocked: %s exists but is not a git clone.\n' "$PROJECT_OS_INSTALL_DIR" >&2
    return 1
  else
    git clone --depth 1 "$PROJECT_OS_REPO_URL" "$PROJECT_OS_INSTALL_DIR" >/dev/null 2>&1 || return 1
  fi

  if _is_project_os_root "$PROJECT_OS_INSTALL_DIR"; then
    printf '%s\n' "$PROJECT_OS_INSTALL_DIR"
    return 0
  fi
  return 1
}

project_os_root() {
  local root=""
  root="$(_discover_project_os_root || true)"
  if [[ -z "$root" ]]; then
    root="$(_install_project_os_root || true)"
  fi
  if [[ -z "$root" ]]; then
    printf 'Could not locate project-os repository. Set PROJECT_OS_ROOT or explicitly allow remote install (PROJECT_OS_AUTO_SETUP=1 and PROJECT_OS_ALLOW_REMOTE_INSTALL=1).\n' >&2
    return 1
  fi
  printf '%s\n' "$root"
}

project_os_python() {
  local root="${1:-}"
  if [[ -n "$root" && -x "$root/.venv/bin/python" ]]; then
    printf '%s\n' "$root/.venv/bin/python"
    return 0
  fi
  if command -v python3 >/dev/null 2>&1; then
    printf 'python3\n'
    return 0
  fi
  if command -v python >/dev/null 2>&1; then
    printf 'python\n'
    return 0
  fi
  printf 'Python is required but was not found in PATH.\n' >&2
  return 1
}

project_os_cli() {
  local root
  local py
  root="$(project_os_root)"
  py="$(project_os_python "$root")"
  (
    cd "$root"
    "$py" -m project_os.cli --db "$PROJECT_OS_DB" --config "$PROJECT_OS_CONFIG" "$@"
  )
}

print_runtime_config() {
  local root
  root="$(project_os_root)"
  printf 'PROJECT_OS_ROOT=%s\n' "$root"
  printf 'PROJECT_OS_DB=%s\n' "$PROJECT_OS_DB"
  printf 'PROJECT_OS_CONFIG=%s\n' "$PROJECT_OS_CONFIG"
  printf 'PROJECT_OS_MEMORY=%s\n' "$PROJECT_OS_MEMORY"
  printf 'DASHBOARD_URL=http://%s:%s\n' "$PROJECT_OS_HOST" "$PROJECT_OS_PORT"
}


path: scripts/daemonize_command.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path


def _parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Daemonize command via double-fork")
    parser.add_argument("--log", required=True, help="Log file path")
    parser.add_argument("--pid-file", required=True, help="PID file path")
    parser.add_argument("--cwd", default="/", help="Working directory")
    parser.add_argument("cmd", nargs=argparse.REMAINDER, help="Command to run, prefixed by --")
    args = parser.parse_args()

    if args.cmd and args.cmd[0] == "--":
        args.cmd = args.cmd[1:]
    if not args.cmd:
        parser.error("command required after --")
    return args


def main() -> int:
    args = _parse_args()

    log_path = Path(args.log).expanduser()
    pid_path = Path(args.pid_file).expanduser()
    cwd = Path(args.cwd).expanduser()

    log_path.parent.mkdir(parents=True, exist_ok=True)
    pid_path.parent.mkdir(parents=True, exist_ok=True)

    read_fd, write_fd = os.pipe()
    first_pid = os.fork()

    if first_pid > 0:
        os.close(write_fd)
        with os.fdopen(read_fd, "r", encoding="utf-8", errors="ignore") as reader:
            daemon_pid = reader.read().strip()
        os.waitpid(first_pid, 0)
        if not daemon_pid:
            return 1
        print(daemon_pid)
        return 0

    os.close(read_fd)
    os.setsid()
    second_pid = os.fork()

    if second_pid > 0:
        os._exit(0)

    try:
        os.chdir(str(cwd))
    except Exception:
        os.chdir("/")

    os.umask(0o022)

    with log_path.open("ab", buffering=0) as log_file:
        with open(os.devnull, "rb", buffering=0) as devnull:
            os.dup2(devnull.fileno(), sys.stdin.fileno())
            os.dup2(log_file.fileno(), sys.stdout.fileno())
            os.dup2(log_file.fileno(), sys.stderr.fileno())

    try:
        pid_path.write_text(f"{os.getpid()}\n", encoding="utf-8")
    except OSError:
        pass

    with os.fdopen(write_fd, "w", encoding="utf-8", errors="ignore") as writer:
        writer.write(str(os.getpid()))
        writer.flush()

    os.execvp(args.cmd[0], args.cmd)


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/easy_mode.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

usage() {
  cat <<'USAGE'
Project Organizer Easy Mode (for non-technical users)

Usage:
  easy_mode.sh setup [quick|full]
  easy_mode.sh sync
  easy_mode.sh projects [limit]
  easy_mode.sh all [limit]
  easy_mode.sh focus [limit]
  easy_mode.sh stale [days] [limit]
  easy_mode.sh weekly
  easy_mode.sh notify [daily|weekly]
  easy_mode.sh today
  easy_mode.sh yesterday
  easy_mode.sh activity
  easy_mode.sh dashboard [start|status|stop]
  easy_mode.sh memory
  easy_mode.sh inbox "<freeform note>" [project-name-or-id]
  easy_mode.sh checkin "<project>" "<done>" "<next>" ["<blocker>"]
  easy_mode.sh create "<project>" ["<what it is>"] ["<next step>"]
  easy_mode.sh resume "<project>"
  easy_mode.sh duplicates
  easy_mode.sh merge <keep-id> <drop-id>
  easy_mode.sh ask "<plain language command>"
  easy_mode.sh blockers [limit]
  easy_mode.sh items [project-name-or-id]
  easy_mode.sh update "<project>" "<what changed>"
  easy_mode.sh next "<project>" "<next step>"
  easy_mode.sh blocker "<project>" "<what is blocking>"
  easy_mode.sh remind "<project>" "<reminder text>" [YYYY-MM-DD]
  easy_mode.sh idea "<project>" "<idea text>"
  easy_mode.sh status "<project>" <now|later|blocked|done>
  easy_mode.sh done <item-id>

Examples:
  easy_mode.sh setup
  easy_mode.sh projects 20
  easy_mode.sh focus
  easy_mode.sh inbox "idea: add auth to landing page"
  easy_mode.sh checkin "project-os" "fixed dashboard filters" "run smoke tests"
  easy_mode.sh today
  easy_mode.sh update "project-os" "Fixed dashboard filtering"
  easy_mode.sh next "project-os" "Test OpenClaw skill end to end"
  easy_mode.sh blocker "project-os" "Waiting on API key"
  easy_mode.sh remind "project-os" "Ship MVP to first users" 2026-03-01
  easy_mode.sh done 42
USAGE
}

require_value() {
  local label="$1"
  local value="${2:-}"
  if [[ -z "$value" ]]; then
    printf 'Missing required %s\n' "$label" >&2
    exit 1
  fi
}

is_number() {
  [[ "$1" =~ ^[0-9]+$ ]]
}

resolve_project_ref() {
  local raw="$1"
  if is_number "$raw"; then
    printf '%s\n' "$raw"
    return 0
  fi

  local root py result status
  root="$(project_os_root)"
  py="$(project_os_python "$root")"

  set +e
  result="$(
    "$py" - "$PROJECT_OS_DB" "$raw" <<'PY'
import sqlite3
import sys
from pathlib import Path

db_path = str(Path(sys.argv[1]).expanduser())
raw = sys.argv[2].strip()
needle = raw.lower()

conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row

def fetch(query, params):
    return conn.execute(query, params).fetchall()

exact_rows = fetch(
    """
    SELECT id, name, status, COALESCE(last_activity_at, updated_at) AS sort_key
    FROM projects
    WHERE lower(name) = ? OR lower(canonical_key) = ?
    ORDER BY sort_key DESC
    """,
    (needle, needle),
)

if len(exact_rows) == 1:
    print(exact_rows[0]["id"])
    raise SystemExit(0)

if len(exact_rows) > 1:
    for row in exact_rows[:10]:
        print(f"{row['id']} | {row['name']} | {row['status']}")
    raise SystemExit(2)

like = f"%{needle}%"
partial_rows = fetch(
    """
    SELECT id, name, status, COALESCE(last_activity_at, updated_at) AS sort_key
    FROM projects
    WHERE lower(name) LIKE ? OR lower(canonical_key) LIKE ?
    ORDER BY sort_key DESC
    LIMIT 10
    """,
    (like, like),
)

if len(partial_rows) == 1:
    print(partial_rows[0]["id"])
    raise SystemExit(0)

if partial_rows:
    for row in partial_rows:
        print(f"{row['id']} | {row['name']} | {row['status']}")
    raise SystemExit(2)

raise SystemExit(3)
PY
  )"
  status=$?
  set -e

  if [[ "$status" -eq 0 ]]; then
    printf '%s\n' "$result"
    return 0
  fi

  if [[ "$status" -eq 2 ]]; then
    printf 'Project name is ambiguous: "%s"\n' "$raw" >&2
    printf 'Use one of these project IDs:\n' >&2
    while IFS= read -r line; do
      [[ -n "$line" ]] || continue
      printf -- '- %s\n' "$line" >&2
    done <<<"$result"
    return 1
  fi

  printf 'Project not found: "%s"\n' "$raw" >&2
  printf 'Tip: run `easy_mode.sh projects` or `easy_mode.sh all` to find the right name/id.\n' >&2
  return 1
}

ensure_ready() {
  if [[ -f "$PROJECT_OS_DB" && -f "$PROJECT_OS_CONFIG" ]]; then
    return 0
  fi
  printf 'First-time setup: preparing your project organizer. This may take a minute...\n'
  "$SCRIPT_DIR/bootstrap.sh" --compact --no-status
}

dashboard_status() {
  local listening="no"
  local pid=""
  if command -v lsof >/dev/null 2>&1; then
    pid="$(lsof -ti "tcp:$PROJECT_OS_PORT" 2>/dev/null | head -n 1 || true)"
    if [[ -n "$pid" ]]; then
      listening="yes"
    fi
  fi
  printf 'Dashboard URL: http://%s:%s\n' "$PROJECT_OS_HOST" "$PROJECT_OS_PORT"
  printf 'Port listening: %s\n' "$listening"
  if [[ -n "$pid" ]]; then
    printf 'Dashboard PID: %s\n' "$pid"
  fi
}

cmd="${1:-help}"
if [[ $# -gt 0 ]]; then
  shift
fi

case "$cmd" in
  setup)
    mode="${1:-full}"
    case "$mode" in
      quick)
        "$SCRIPT_DIR/bootstrap.sh" --quick --compact --no-status
        ;;
      full)
        "$SCRIPT_DIR/bootstrap.sh" --compact --no-status
        ;;
      *)
        printf 'Unknown setup mode: %s (use quick or full)\n' "$mode" >&2
        exit 1
        ;;
    esac
    ;;

  sync)
    ensure_ready
    "$SCRIPT_DIR/run_refresh.sh" --compact
    "$SCRIPT_DIR/write_memory.sh"
    printf 'Sync complete.\n'
    ;;

  projects)
    ensure_ready
    limit="${1:-40}"
    if ! is_number "$limit"; then
      printf 'Limit must be a number.\n' >&2
      exit 1
    fi
    "$SCRIPT_DIR/project_actions.sh" list --status active --limit "$limit"
    ;;

  all)
    ensure_ready
    limit="${1:-80}"
    if ! is_number "$limit"; then
      printf 'Limit must be a number.\n' >&2
      exit 1
    fi
    "$SCRIPT_DIR/project_actions.sh" list --limit "$limit"
    ;;

  today)
    ensure_ready
    "$SCRIPT_DIR/project_actions.sh" activity --when today --limit 80 --evidence 2
    ;;

  yesterday)
    ensure_ready
    "$SCRIPT_DIR/project_actions.sh" activity --when yesterday --limit 80 --evidence 2
    ;;

  activity)
    ensure_ready
    "$SCRIPT_DIR/project_actions.sh" activity --when both --limit 80 --evidence 2
    ;;

  focus)
    ensure_ready
    limit="${1:-3}"
    if ! is_number "$limit"; then
      printf 'Limit must be a number.\n' >&2
      exit 1
    fi
    "$SCRIPT_DIR/project_actions.sh" focus --limit "$limit"
    ;;

  stale)
    ensure_ready
    days="${1:-14}"
    limit="${2:-20}"
    if ! is_number "$days" || ! is_number "$limit"; then
      printf 'Days and limit must be numbers.\n' >&2
      exit 1
    fi
    "$SCRIPT_DIR/project_actions.sh" stale --days "$days" --limit "$limit"
    ;;

  weekly)
    ensure_ready
    "$SCRIPT_DIR/project_actions.sh" weekly --days 7 --limit 12
    ;;

  notify)
    ensure_ready
    period="${1:-daily}"
    "$SCRIPT_DIR/project_actions.sh" notify --period "$period"
    ;;

  dashboard)
    ensure_ready
    action="${1:-start}"
    case "$action" in
      start)
        "$SCRIPT_DIR/start_dashboard.sh" --detach --restart
        dashboard_status
        ;;
      status)
        dashboard_status
        ;;
      stop)
        pid=""
        if command -v lsof >/dev/null 2>&1; then
          pid="$(lsof -ti "tcp:$PROJECT_OS_PORT" 2>/dev/null | head -n 1 || true)"
        fi
        if [[ -n "$pid" ]]; then
          kill "$pid"
          printf 'Stopped dashboard PID %s\n' "$pid"
        else
          printf 'Dashboard is not running.\n'
        fi
        ;;
      *)
        printf 'Unknown dashboard action: %s (use start, status, stop)\n' "$action" >&2
        exit 1
        ;;
    esac
    ;;

  memory)
    ensure_ready
    "$SCRIPT_DIR/write_memory.sh"
    printf 'Memory file: %s\n' "$PROJECT_OS_MEMORY"
    ;;

  inbox)
    ensure_ready
    text="${1:-}"
    project="${2:-}"
    require_value "text" "$text"
    if [[ -n "$project" ]]; then
      resolved_project="$(resolve_project_ref "$project")"
      "$SCRIPT_DIR/project_actions.sh" inbox --text "$text" --project "$resolved_project"
    else
      "$SCRIPT_DIR/project_actions.sh" inbox --text "$text"
    fi
    ;;

  checkin)
    ensure_ready
    project="${1:-}"
    done_text="${2:-}"
    next_text="${3:-}"
    blocker_text="${4:-}"
    require_value "project" "$project"
    require_value "done" "$done_text"
    require_value "next" "$next_text"
    resolved_project="$(resolve_project_ref "$project")"
    args=(checkin --project "$resolved_project" --done "$done_text" --next "$next_text")
    if [[ -n "$blocker_text" ]]; then
      args+=(--blocker "$blocker_text")
    fi
    "$SCRIPT_DIR/project_actions.sh" "${args[@]}"
    ;;

  create)
    ensure_ready
    name="${1:-}"
    summary="${2:-}"
    next_text="${3:-}"
    require_value "project name" "$name"
    args=(create --name "$name")
    if [[ -n "$summary" ]]; then
      args+=(--summary "$summary")
    fi
    if [[ -n "$next_text" ]]; then
      args+=(--next "$next_text")
    fi
    "$SCRIPT_DIR/project_actions.sh" "${args[@]}"
    ;;

  resume)
    ensure_ready
    project="${1:-}"
    require_value "project" "$project"
    resolved_project="$(resolve_project_ref "$project")"
    "$SCRIPT_DIR/project_actions.sh" resume --project "$resolved_project" --evidence 3
    ;;

  duplicates)
    ensure_ready
    "$SCRIPT_DIR/project_actions.sh" duplicates --limit 40
    ;;

  merge)
    ensure_ready
    keep="${1:-}"
    drop="${2:-}"
    require_value "keep project id" "$keep"
    require_value "drop project id" "$drop"
    "$SCRIPT_DIR/project_actions.sh" merge --keep "$keep" --drop "$drop"
    ;;

  ask)
    ensure_ready
    text="${1:-}"
    require_value "text" "$text"
    "$SCRIPT_DIR/project_actions.sh" ask --text "$text"
    ;;

  blockers)
    ensure_ready
    limit="${1:-40}"
    if ! is_number "$limit"; then
      printf 'Limit must be a number.\n' >&2
      exit 1
    fi
    "$SCRIPT_DIR/project_actions.sh" blockers --limit "$limit"
    ;;

  items)
    ensure_ready
    project="${1:-}"
    if [[ -n "$project" ]]; then
      resolved_project="$(resolve_project_ref "$project")"
      "$SCRIPT_DIR/project_actions.sh" list-items --project "$resolved_project" --status open --limit 80
    else
      "$SCRIPT_DIR/project_actions.sh" list-items --status open --limit 80
    fi
    ;;

  update|next|idea|blocker)
    ensure_ready
    project="${1:-}"
    text="${2:-}"
    require_value "project" "$project"
    require_value "text" "$text"
    resolved_project="$(resolve_project_ref "$project")"
    case "$cmd" in
      update) "$SCRIPT_DIR/project_actions.sh" add-update --project "$resolved_project" --text "$text" ;;
      next) "$SCRIPT_DIR/project_actions.sh" add-next --project "$resolved_project" --text "$text" ;;
      idea) "$SCRIPT_DIR/project_actions.sh" add-idea --project "$resolved_project" --text "$text" ;;
      blocker) "$SCRIPT_DIR/project_actions.sh" add-blocker --project "$resolved_project" --text "$text" ;;
    esac
    ;;

  remind)
    ensure_ready
    project="${1:-}"
    text="${2:-}"
    due="${3:-}"
    require_value "project" "$project"
    require_value "text" "$text"
    resolved_project="$(resolve_project_ref "$project")"
    if [[ -n "$due" ]]; then
      "$SCRIPT_DIR/project_actions.sh" add-reminder --project "$resolved_project" --text "$text" --due "$due"
    else
      "$SCRIPT_DIR/project_actions.sh" add-reminder --project "$resolved_project" --text "$text"
    fi
    ;;

  status)
    ensure_ready
    project="${1:-}"
    state="${2:-}"
    require_value "project" "$project"
    require_value "status" "$state"
    resolved_project="$(resolve_project_ref "$project")"
    "$SCRIPT_DIR/project_actions.sh" simple-status --project "$resolved_project" --status "$state"
    ;;

  done)
    ensure_ready
    item_id="${1:-}"
    require_value "item-id" "$item_id"
    "$SCRIPT_DIR/project_actions.sh" set-item --item "$item_id" --status done
    ;;

  help|--help|-h)
    usage
    ;;

  *)
    printf 'Unknown command: %s\n' "$cmd" >&2
    usage >&2
    exit 1
    ;;
esac


path: scripts/openclaw_smoke.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

strict=1
while [[ $# -gt 0 ]]; do
  case "$1" in
    --agent-safe)
      strict=0
      shift
      ;;
    --strict)
      strict=1
      shift
      ;;
    --help|-h)
      cat <<'USAGE'
Usage: openclaw_smoke.sh [--agent-safe|--strict]

Modes:
  --strict      Exit non-zero on failure (default)
  --agent-safe  Always exit zero and return RESULT_STATUS/RESULT_ERROR lines
USAGE
      exit 0
      ;;
    *)
      printf 'Unknown option: %s\n' "$1" >&2
      exit 1
      ;;
  esac
done

mkdir -p "$HOME/.project_os"
smoke_log="${PROJECT_OS_SMOKE_LOG:-$HOME/.project_os/openclaw_smoke.log}"
result_file="${PROJECT_OS_SMOKE_RESULT_FILE:-$HOME/.project_os/openclaw_smoke_result.txt}"
dashboard_log="$HOME/.project_os/project_os_dashboard.log"
pid_file="$HOME/.project_os/project_os_dashboard.pid"

status="ok"
error=""
dashboard_pid=""
port_listening="unknown"
curl_head="n/a"
dashboard_url="http://$PROJECT_OS_HOST:$PROJECT_OS_PORT"

set_error() {
  status="error"
  error="$1"
}

wait_for_http() {
  local tries="$1"
  local pid="$2"
  local i
  for ((i=1; i<=tries; i++)); do
    if ! kill -0 "$pid" 2>/dev/null; then
      return 1
    fi
    if curl -s --max-time 1 "$dashboard_url" >/dev/null 2>&1; then
      return 0
    fi
    sleep 1
  done
  return 1
}

emit_results() {
  local tmp_file
  tmp_file="$(mktemp "${result_file}.XXXXXX")"
  {
    printf 'RESULT_STATUS=%s\n' "$status"
    printf 'RESULT_ERROR=%s\n' "${error:-none}"
    printf 'RESULT_DASHBOARD_URL=%s\n' "$dashboard_url"
    printf 'RESULT_DASHBOARD_PID=%s\n' "${dashboard_pid:-none}"
    printf 'RESULT_PORT_8765_LISTENING=%s\n' "$port_listening"
    printf 'RESULT_CURL_HEAD=%s\n' "$curl_head"
    printf 'RESULT_SMOKE_LOG=%s\n' "$smoke_log"
    printf 'RESULT_DASHBOARD_LOG=%s\n' "$dashboard_log"
    printf 'RESULT_RESULT_FILE=%s\n' "$result_file"
  } >"$tmp_file"
  mv "$tmp_file" "$result_file"
  cat "$result_file"
}

if ! "$SCRIPT_DIR/bootstrap.sh" --noninteractive >"$smoke_log" 2>&1; then
  set_error "bootstrap_failed"
fi

if [[ "$status" == "ok" ]]; then
  if ! "$SCRIPT_DIR/start_dashboard.sh" --detach --restart >>"$smoke_log" 2>&1; then
    set_error "dashboard_start_failed"
  fi
fi

if [[ "$status" == "ok" ]]; then
  dashboard_pid="$(cat "$pid_file" 2>/dev/null || true)"
  if [[ -z "$dashboard_pid" ]]; then
    set_error "missing_pid_file"
  fi
fi

if [[ "$status" == "ok" && -n "$dashboard_pid" ]]; then
  if ! wait_for_http 15 "$dashboard_pid"; then
    set_error "http_readiness_failed"
  fi
fi

if [[ "$status" == "ok" && -n "$dashboard_pid" ]]; then
  sleep 3
  if ! kill -0 "$dashboard_pid" 2>/dev/null; then
    set_error "dashboard_exited_after_start"
  fi
fi

if command -v lsof >/dev/null 2>&1; then
  if lsof -i "tcp:$PROJECT_OS_PORT" -sTCP:LISTEN -n -P >/dev/null 2>&1; then
    port_listening="yes"
  else
    port_listening="no"
  fi
fi

if command -v curl >/dev/null 2>&1; then
  curl_head="$(curl -s --max-time 5 "$dashboard_url" | head -n 3 | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g' | sed -E 's/[[:space:]]+$//' || true)"
  if [[ -z "$curl_head" ]]; then
    curl_head="n/a"
  fi
fi

emit_results

if [[ "$status" != "ok" ]]; then
  if [[ -f "$smoke_log" ]]; then
    printf '\nLast log lines:\n' >&2
    tail -n 40 "$smoke_log" >&2 || true
  fi
  if [[ "$strict" -eq 1 ]]; then
    exit 1
  fi
fi


path: scripts/openclaw_smoke_agent.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Agent-safe wrapper for environments where non-zero exit codes can hide output.
"$SCRIPT_DIR/openclaw_smoke.sh" --agent-safe


path: scripts/project_actions.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

usage() {
  cat <<'USAGE'
Usage: project_actions.sh <command> [args]

Commands:
  list [--status active|blocked|paused|archived] [--limit N]
  activity [--when today|yesterday|both] [--limit N] [--evidence N] [--include-archived] [--all-signals] [--ignore-scope] [--json]
  focus [--limit N]
  stale [--days N] [--limit N]
  weekly [--days N] [--limit N]
  notify [--period daily|weekly]
  blockers [--limit N]
  inbox --text "<note text>" [--project <id|name>] [--no-create]
  checkin --project <id|name> [--done "<done>"] [--next "<next>"] [--blocker "<blocker>"]
  create --name "<name>" [--summary "<what>"] [--next "<next step>"] [--status now|later|blocked|done]
  resume --project <id|name> [--evidence N]
  simple-status --project <id|name> --status <now|later|blocked|done>
  duplicates [--limit N]
  merge --keep <id|name> --drop <id|name>
  ask --text "<natural command>"
  set-status --project <id|name> --status <active|blocked|paused|archived>
  set-next --project <id|name> --text "<next action>"
  add-update --project <id|name> --text "<update text>"
  add-next --project <id|name> --text "<next step>"
  add-blocker --project <id|name> --text "<blocker>"
  add-reminder --project <id|name> --text "<reminder text>" [--due YYYY-MM-DD]
  add-idea --project <id|name> --text "<idea text>"
  track --project <id|name>
  mute --project <id|name>
  untrack --project <id|name>
  scope [--set <id|name> ...] [--limit N]
  list-items [--project <id|name>] [--kind update|next|reminder|idea|blocker] [--status open|done|dismissed|all] [--limit N]
  set-item --item <id> --status <open|done|dismissed>

Examples:
  project_actions.sh list --limit 60
  project_actions.sh activity --when both
  project_actions.sh focus --limit 3
  project_actions.sh inbox --text "remind me to ship on 2026-03-01"
  project_actions.sh set-status --project "project-os" --status blocked
  project_actions.sh add-next --project 23 --text "Validate dashboard edit flow"
  project_actions.sh set-item --item 101 --status done
USAGE
}

require_value() {
  local label="$1"
  local value="${2:-}"
  if [[ -z "$value" ]]; then
    printf 'Missing required %s\n' "$label" >&2
    exit 1
  fi
}

cmd="${1:-}"
if [[ -z "$cmd" ]]; then
  usage
  exit 1
fi
shift || true

case "$cmd" in
  list)
    status=""
    limit="80"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --status) status="${2:-}"; shift 2 ;;
        --limit) limit="${2:-80}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    if [[ -n "$status" ]]; then
      project_os_cli list-projects --status "$status" --limit "$limit"
    else
      project_os_cli list-projects --limit "$limit"
    fi
    ;;
  activity)
    when="both"
    limit="80"
    evidence="2"
    include_archived=0
    all_signals=0
    ignore_scope=0
    json_mode=0
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --when) when="${2:-both}"; shift 2 ;;
        --limit) limit="${2:-80}"; shift 2 ;;
        --evidence) evidence="${2:-2}"; shift 2 ;;
        --include-archived) include_archived=1; shift ;;
        --all-signals) all_signals=1; shift ;;
        --ignore-scope) ignore_scope=1; shift ;;
        --json) json_mode=1; shift ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    args=(activity-report --when "$when" --limit "$limit" --evidence "$evidence")
    if [[ "$include_archived" -eq 1 ]]; then
      args+=(--include-archived)
    fi
    if [[ "$all_signals" -eq 1 ]]; then
      args+=(--all-signals)
    fi
    if [[ "$ignore_scope" -eq 1 ]]; then
      args+=(--ignore-scope)
    fi
    if [[ "$json_mode" -eq 1 ]]; then
      args+=(--json)
    fi
    project_os_cli "${args[@]}"
    ;;
  track)
    project=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    project_os_cli set-tracking --project "$project" --mode track
    ;;
  mute)
    project=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    project_os_cli set-tracking --project "$project" --mode mute
    ;;
  untrack)
    project=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    project_os_cli set-tracking --project "$project" --mode auto
    ;;
  scope)
    limit="300"
    mode="list"
    projects=()
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --set)
          mode="set"
          shift
          while [[ $# -gt 0 && "${1:0:2}" != "--" ]]; do
            projects+=("$1")
            shift
          done
          ;;
        --limit) limit="${2:-300}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    if [[ "$mode" == "set" ]]; then
      if [[ "${#projects[@]}" -eq 0 ]]; then
        printf 'Missing required project names after --set\n' >&2
        exit 1
      fi
      args=(set-tracking-scope)
      for project in "${projects[@]}"; do
        args+=(--project "$project")
      done
      project_os_cli "${args[@]}"
    else
      project_os_cli list-tracking --limit "$limit"
    fi
    ;;
  focus)
    limit="3"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --limit) limit="${2:-3}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    project_os_cli focus-list --limit "$limit"
    ;;
  stale)
    days="14"
    limit="20"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --days) days="${2:-14}"; shift 2 ;;
        --limit) limit="${2:-20}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    project_os_cli stale-nudges --days "$days" --limit "$limit"
    ;;
  weekly)
    days="7"
    limit="12"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --days) days="${2:-7}"; shift 2 ;;
        --limit) limit="${2:-12}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    project_os_cli weekly-review --days "$days" --limit "$limit"
    ;;
  notify)
    period="daily"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --period) period="${2:-daily}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    project_os_cli notification-summary --period "$period"
    ;;
  blockers)
    limit="80"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --limit) limit="${2:-80}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    project_os_cli blocker-report --limit "$limit"
    ;;
  inbox)
    text=""
    project=""
    no_create=0
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --text) text="${2:-}"; shift 2 ;;
        --project) project="${2:-}"; shift 2 ;;
        --no-create) no_create=1; shift ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "text" "$text"
    args=(inbox-capture --text "$text")
    if [[ -n "$project" ]]; then
      args+=(--project "$project")
    fi
    if [[ "$no_create" -eq 1 ]]; then
      args+=(--no-create)
    fi
    project_os_cli "${args[@]}"
    ;;
  checkin)
    project=""
    done_text=""
    next_text=""
    blocker_text=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        --done) done_text="${2:-}"; shift 2 ;;
        --next) next_text="${2:-}"; shift 2 ;;
        --blocker) blocker_text="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    args=(daily-checkin --project "$project")
    if [[ -n "$done_text" ]]; then
      args+=(--done "$done_text")
    fi
    if [[ -n "$next_text" ]]; then
      args+=(--next-step "$next_text")
    fi
    if [[ -n "$blocker_text" ]]; then
      args+=(--blocker "$blocker_text")
    fi
    project_os_cli "${args[@]}"
    ;;
  create)
    name=""
    summary=""
    next_text=""
    status="now"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --name) name="${2:-}"; shift 2 ;;
        --summary) summary="${2:-}"; shift 2 ;;
        --next) next_text="${2:-}"; shift 2 ;;
        --status) status="${2:-now}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "name" "$name"
    args=(create-project --name "$name" --status "$status")
    if [[ -n "$summary" ]]; then
      args+=(--summary "$summary")
    fi
    if [[ -n "$next_text" ]]; then
      args+=(--next-action "$next_text")
    fi
    project_os_cli "${args[@]}"
    ;;
  resume)
    project=""
    evidence="3"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        --evidence) evidence="${2:-3}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    project_os_cli resume-pack --project "$project" --evidence "$evidence"
    ;;
  simple-status)
    project=""
    status=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        --status) status="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    require_value "status" "$status"
    project_os_cli set-simple-status --project "$project" --status "$status"
    ;;
  duplicates)
    limit="50"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --limit) limit="${2:-50}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    project_os_cli detect-duplicates --limit "$limit"
    ;;
  merge)
    keep=""
    drop=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --keep) keep="${2:-}"; shift 2 ;;
        --drop) drop="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "keep" "$keep"
    require_value "drop" "$drop"
    project_os_cli merge-projects --keep "$keep" --drop "$drop"
    ;;
  ask)
    text=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --text) text="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "text" "$text"
    project_os_cli quick-command --text "$text"
    ;;
  set-status)
    project=""
    status=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        --status) status="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    require_value "status" "$status"
    project_os_cli set-status --project "$project" --status "$status"
    ;;
  set-next)
    project=""
    text=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        --text) text="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    require_value "text" "$text"
    project_os_cli set-next-action --project "$project" --next-action "$text"
    ;;
  add-update|add-next|add-reminder|add-idea|add-blocker)
    project=""
    text=""
    due=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        --text) text="${2:-}"; shift 2 ;;
        --due) due="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "project" "$project"
    require_value "text" "$text"
    case "$cmd" in
      add-update) project_os_cli add-update --project "$project" --text "$text" ;;
      add-next) project_os_cli add-next --project "$project" --text "$text" ;;
      add-idea) project_os_cli add-idea --project "$project" --text "$text" ;;
      add-blocker) project_os_cli add-item --project "$project" --kind blocker --text "$text" ;;
      add-reminder)
        if [[ -n "$due" ]]; then
          project_os_cli add-reminder --project "$project" --text "$text" --due "$due"
        else
          project_os_cli add-reminder --project "$project" --text "$text"
        fi
        ;;
    esac
    ;;
  list-items)
    project=""
    kind=""
    status="open"
    limit="40"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --project) project="${2:-}"; shift 2 ;;
        --kind) kind="${2:-}"; shift 2 ;;
        --status) status="${2:-open}"; shift 2 ;;
        --limit) limit="${2:-40}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    args=(list-items --status "$status" --limit "$limit")
    if [[ -n "$project" ]]; then
      args+=(--project "$project")
    fi
    if [[ -n "$kind" ]]; then
      args+=(--kind "$kind")
    fi
    project_os_cli "${args[@]}"
    ;;
  set-item)
    item=""
    status=""
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --item) item="${2:-}"; shift 2 ;;
        --status) status="${2:-}"; shift 2 ;;
        *) printf 'Unknown arg: %s\n' "$1" >&2; exit 1 ;;
      esac
    done
    require_value "item" "$item"
    require_value "status" "$status"
    project_os_cli set-item-status --item-id "$item" --status "$status"
    ;;
  --help|-h|help)
    usage
    ;;
  *)
    printf 'Unknown command: %s\n' "$cmd" >&2
    usage >&2
    exit 1
    ;;
esac


path: scripts/project_router.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

usage() {
  cat <<'USAGE'
Usage:
  project_router.sh "<user message>"

Examples:
  project_router.sh "What am I working on today?"
  project_router.sh "Show my projects"
  project_router.sh "Next for project-os: run smoke tests"
USAGE
}

if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
  usage
  exit 0
fi

message="$*"
if [[ -z "$message" ]]; then
  message="show my projects"
fi

if [[ ! -f "$PROJECT_OS_DB" || ! -f "$PROJECT_OS_CONFIG" ]]; then
  "$SCRIPT_DIR/bootstrap.sh" --quick --compact --no-status >/dev/null
fi

root="$(project_os_root)"
py="$(project_os_python "$root")"
(
  cd "$root"
  "$py" -m project_os.router \
    --db "$PROJECT_OS_DB" \
    --message "$message" \
    --scripts-dir "$SCRIPT_DIR" \
    --host "$PROJECT_OS_HOST" \
    --port "$PROJECT_OS_PORT"
)


path: scripts/quick_status.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

project_os_cli list-projects --limit 80
project_os_cli list-sessions --limit 25
project_os_cli list-items --status open --limit 40


path: scripts/read_smoke_result.sh
content: #!/usr/bin/env bash
set -euo pipefail

result_file="${PROJECT_OS_SMOKE_RESULT_FILE:-$HOME/.project_os/openclaw_smoke_result.txt}"

if [[ ! -f "$result_file" ]]; then
  printf 'Smoke result file not found: %s\n' "$result_file" >&2
  printf 'Run scripts/openclaw_smoke.sh first.\n' >&2
  exit 1
fi

cat "$result_file"


path: scripts/run_refresh.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

compact="${PROJECT_OS_COMPACT:-0}"
if [[ "${1:-}" == "--compact" ]]; then
  compact=1
fi

"$SCRIPT_DIR/setup_test_config.py"
project_os_cli init
project_os_cli refresh
if [[ "$compact" -eq 0 ]]; then
  project_os_cli list-projects --limit 140
fi


path: scripts/setup_test_config.py
content: #!/usr/bin/env python3
from __future__ import annotations

import json
import os
import subprocess
from pathlib import Path

HOME = Path.home()
PROJECT_OS_HOME = HOME / ".project_os"
CONFIG_PATH = Path(os.environ.get("PROJECT_OS_CONFIG", str(PROJECT_OS_HOME / "openclaw_test_config.json")))
DB_PATH = Path(os.environ.get("PROJECT_OS_DB", str(PROJECT_OS_HOME / "openclaw_test.db")))
MEMORY_PATH = Path(os.environ.get("PROJECT_OS_MEMORY", str(PROJECT_OS_HOME / "PROJECT_MEMORY.md")))

CANDIDATE_ROOTS = [
    HOME / "GitHub",
    HOME / "GitHub-Organized",
    HOME / "claude-workspaces",
    HOME / "Claude Code",
    HOME / ".openclaw" / "workspace",
]

CANDIDATE_CONVERSATION_ROOTS = [
    HOME / ".claude",
    HOME / "Claude Code",
    HOME / ".codex",
    HOME / ".openclaw",
]


def bool_env(name: str, default: bool = False) -> bool:
    raw = os.environ.get(name, "")
    if not raw.strip():
        return default
    return raw.strip().lower() in {"1", "true", "yes", "on"}


def existing_dirs(paths: list[Path]) -> list[str]:
    out: list[str] = []
    for path in paths:
        if path.exists() and path.is_dir():
            out.append(str(path))
    return out


def auto_extra_roots() -> list[str]:
    extras: list[str] = []
    for child in HOME.iterdir():
        if not child.is_dir():
            continue
        lowered = child.name.lower()
        if any(token in lowered for token in ["workspace", "projects", "github", "repos"]):
            extras.append(str(child))
    return sorted(set(extras))


def env_extra_roots() -> list[str]:
    raw = os.environ.get("PROJECT_OS_EXTRA_ROOTS", "")
    if not raw.strip():
        return []
    extras: list[str] = []
    for segment in raw.split(":"):
        candidate = Path(segment.strip()).expanduser()
        if candidate.exists() and candidate.is_dir():
            extras.append(str(candidate))
    return sorted(set(extras))


def detect_github_username(enabled: bool) -> str:
    if not enabled:
        return ""

    env_name = os.environ.get("GITHUB_USERNAME", "").strip()
    if env_name:
        return env_name

    try:
        proc = subprocess.run(
            ["gh", "api", "user", "--jq", ".login"],
            capture_output=True,
            text=True,
            timeout=4,
            check=False,
        )
        if proc.returncode == 0:
            login = proc.stdout.strip()
            if login:
                return login
    except Exception:
        pass

    return ""


def main() -> int:
    PROJECT_OS_HOME.mkdir(parents=True, exist_ok=True)
    CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    MEMORY_PATH.parent.mkdir(parents=True, exist_ok=True)

    privacy_mode = os.environ.get("PROJECT_OS_PRIVACY_MODE", "strict").strip().lower() or "strict"
    enable_home_discovery = bool_env("PROJECT_OS_ENABLE_HOME_DISCOVERY", False)
    include_chat_roots = bool_env("PROJECT_OS_INCLUDE_CHAT_ROOTS", False)
    enable_github_sync = bool_env("PROJECT_OS_ENABLE_GITHUB_SYNC", False)

    local_roots = existing_dirs(CANDIDATE_ROOTS)
    if enable_home_discovery:
        local_roots.extend(auto_extra_roots())
    local_roots.extend(env_extra_roots())
    local_roots = sorted(set(local_roots))

    conversation_roots: list[str] = []
    if include_chat_roots:
        conversation_roots = sorted(set(existing_dirs(CANDIDATE_CONVERSATION_ROOTS)))

    github_username = detect_github_username(enable_github_sync)
    github_token_env_var = "GITHUB_TOKEN" if enable_github_sync else ""

    config = {
        "local_roots": local_roots,
        "include_non_git_projects": True,
        "include_nested_non_git_projects": False,
        "prune_missing_local_projects": True,
        "include_collection_projects": True,
        "collection_dir_names": ["codex_projects"],
        "ignore_project_path_patterns": [
            r"/\.openclaw/workspace/skills/",
            r"/\.codex/skills/",
            r"/clawd/skills/",
        ],
        "github": {"username": github_username, "token_env_var": github_token_env_var},
        "conversation_roots": conversation_roots,
        "session_index_max_files": 1500 if privacy_mode == "strict" else 3500,
        "create_chat_projects": bool(include_chat_roots),
        "auto_squash_chat_projects": True,
        "prune_missing_sessions": True,
        "memory_file_path": str(MEMORY_PATH),
        "privacy_mode": privacy_mode,
        "servers": [],
    }

    CONFIG_PATH.write_text(json.dumps(config, indent=2), encoding="utf-8")

    print(f"Wrote config: {CONFIG_PATH}")
    print(f"DB path: {DB_PATH}")
    print("Local roots:")
    for root in local_roots:
        print(f"- {root}")
    print("Conversation roots:")
    for root in conversation_roots:
        print(f"- {root}")
    if not conversation_roots:
        print("- (disabled by default; set PROJECT_OS_INCLUDE_CHAT_ROOTS=1 to enable)")
    github_name = config["github"]["username"] or "(disabled)"
    print(f"GitHub username: {github_name}")
    print(f"Privacy mode: {privacy_mode}")
    print(f"Home auto-discovery: {'enabled' if enable_home_discovery else 'disabled'}")
    print(f"Chat transcript indexing: {'enabled' if include_chat_roots else 'disabled'}")
    print(f"GitHub sync: {'enabled' if enable_github_sync else 'disabled'}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/start_dashboard.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

restart=0
detach=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --restart)
      restart=1
      shift
      ;;
    --detach)
      detach=1
      shift
      ;;
    --help|-h)
      cat <<'USAGE'
Usage: start_dashboard.sh [--restart] [--detach]

Options:
  --restart   Stop existing dashboard first
  --detach    Start dashboard in background and return
USAGE
      exit 0
      ;;
    *)
      printf 'Unknown option: %s\n' "$1" >&2
      exit 1
      ;;
  esac
done

existing_pid=""
if command -v lsof >/dev/null 2>&1; then
  existing_pid="$(lsof -ti "tcp:$PROJECT_OS_PORT" 2>/dev/null | head -n 1 || true)"
fi

if [[ -n "$existing_pid" && "$restart" -eq 0 ]]; then
  printf 'Dashboard already running at http://%s:%s (pid %s)\n' "$PROJECT_OS_HOST" "$PROJECT_OS_PORT" "$existing_pid"
  exit 0
fi

if [[ -n "$existing_pid" && "$restart" -eq 1 ]]; then
  kill "$existing_pid"
  sleep 1
fi

if [[ "$detach" -eq 1 ]]; then
  root="$(project_os_root)"
  py="$(project_os_python)"
  mkdir -p "$HOME/.project_os"
  log_path="$HOME/.project_os/project_os_dashboard.log"
  pid_file="$HOME/.project_os/project_os_dashboard.pid"
  helper="$SCRIPT_DIR/daemonize_command.py"
  if [[ ! -x "$helper" ]]; then
    printf 'Missing executable helper: %s\n' "$helper" >&2
    exit 1
  fi
  new_pid="$(
    "$helper" \
      --log "$log_path" \
      --pid-file "$pid_file" \
      --cwd "$root" \
      -- \
      "$py" -m project_os.cli --db "$PROJECT_OS_DB" --config "$PROJECT_OS_CONFIG" dashboard --host "$PROJECT_OS_HOST" --port "$PROJECT_OS_PORT"
  )"
  if [[ -z "$new_pid" ]]; then
    printf 'Dashboard failed to start. No process id returned.\n' >&2
    tail -n 40 "$log_path" >&2 || true
    exit 1
  fi

  ready=0
  for _ in {1..20}; do
    if ! kill -0 "$new_pid" 2>/dev/null; then
      break
    fi
    if command -v curl >/dev/null 2>&1; then
      if curl -s --max-time 1 "http://$PROJECT_OS_HOST:$PROJECT_OS_PORT" >/dev/null 2>&1; then
        ready=1
        break
      fi
    else
      ready=1
      break
    fi
    sleep 0.5
  done

  if kill -0 "$new_pid" 2>/dev/null; then
    printf 'Dashboard started at http://%s:%s (pid %s)\n' "$PROJECT_OS_HOST" "$PROJECT_OS_PORT" "$new_pid"
    printf 'Log: %s\n' "$log_path"
    if [[ "$ready" -eq 0 ]]; then
      printf 'Warning: server not yet responding; check log and retry.\n' >&2
    fi
    exit 0
  fi
  printf 'Dashboard failed to start. Check log: %s\n' "$log_path" >&2
  tail -n 40 "$log_path" >&2 || true
  exit 1
fi

project_os_cli dashboard --host "$PROJECT_OS_HOST" --port "$PROJECT_OS_PORT"


path: scripts/write_memory.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"

project_os_cli write-memory --out "$PROJECT_OS_MEMORY" --limit 220
printf 'Memory file: %s\n' "$PROJECT_OS_MEMORY"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)