diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 343a488c..d1b614c3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,5 +5,5 @@ "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {} }, - "postCreateCommand": "git --version && node -v && npm -v" -} + "postCreateCommand": "git --version && node -v && npm -v && cp scripts/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit && echo '✅ Pre-commit hook installed'" +} diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..c119e5ad --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# TogetherOS Environment Variables + +# OpenAI API Key (required for Bridge) +OPENAI_API_KEY=sk-your-openai-api-key-here + +# Bridge Configuration +BRIDGE_RATE_LIMIT_PER_HOUR=30 +BRIDGE_IP_SALT=your-random-salt-for-ip-hashing +BRIDGE_ENV=development + +# Database (future) +# DATABASE_URL=postgresql://user:pass@localhost:5432/togetheros + +# Authentication (future) +# NEXTAUTH_URL=http://localhost:3000 +# NEXTAUTH_SECRET=your-nextauth-secret diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..d595189f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,39 @@ +# TogetherOS Code Owners +# +# These owners will be requested for review when someone opens a pull request +# that modifies files in the specified paths. +# +# Syntax: ... +# Owners can be: @username, @org/team-name, email@example.com + +# Default owners for everything (fallback) +* @coopeverything + +# CI/CD and workflows - require maintainer review +/.github/workflows/ @coopeverything +/.github/CODEOWNERS @coopeverything +/scripts/ @coopeverything + +# Core documentation - require maintainer review +/docs/architecture.md @coopeverything +/docs/tech-stack.md @coopeverything +/docs/cooperation-paths.md @coopeverything + +# Configuration files - require careful review +/.devcontainer/ @coopeverything +/.markdownlint.jsonc @coopeverything +/.yamllint.yaml @coopeverything +/package.json @coopeverything +/pnpm-workspace.yaml @coopeverything +/tsconfig*.json @coopeverything + +# Application code - can be owned by specific teams in future +# /apps/web/ @frontend-team +# /apps/api/ @backend-team +# /packages/ @platform-team + +# Module documentation - module maintainers can review +# /docs/modules/bridge.md @bridge-team +# /docs/modules/governance.md @governance-team + +# Comments show future structure as project grows diff --git a/.github/ISSUE_TEMPLATE/codex_task.yml b/.github/ISSUE_TEMPLATE/codex_task.yml deleted file mode 100644 index 22b126d4..00000000 --- a/.github/ISSUE_TEMPLATE/codex_task.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Codex Task -description: Create/update files under codex/* with a strict JSON body and a target proof-line. -title: "codex: " -labels: - - codex -body: - - type: markdown - attributes: - value: | - **Instructions** - - Paste a single strict JSON object (no comments, no trailing commas). - - All file paths **must** be under `codex/`. - - Keep payload minimal. Codex or maintainers will open a PR. - - The **preflight validator** adds `codex-ready` automatically after checks pass. Do **not** add it manually. - - **Minimal schema** - ~~~json - { - "files": [ - { - "path": "codex/", - "op": "create|update", - "content_type": "text|base64", - "content": "" - } - ], - "proof_line": "SOME_PROOF=OK", - "notes": "" - } - ~~~ - - - type: textarea - id: json - attributes: - label: Task JSON (strict) - description: Paste one JSON object exactly as it should be processed. - placeholder: | - { - "files": [ - { - "path": "codex/example.txt", - "op": "create", - "content_type": "text", - "content": "hello" - } - ], - "proof_line": "EXAMPLE=OK", - "notes": "short note" - } - render: text - validations: - required: true - - - type: checkboxes - id: confirm - attributes: - label: Acknowledgements - options: - - label: I confirm the body is a single strict JSON object (no arrays/comments) and `jq -e .` would pass. - required: true - - label: I confirm all paths are under `codex/`. - required: true - - label: I understand the preflight validator will add `codex-ready` if checks pass. - required: true - - label: I understand a "no changes" outcome is valid and may return a proof-line indicating no updates were needed. - required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 48e02c64..66a07245 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -37,4 +37,4 @@ SMOKE=OK - [ ] No secrets or PII added; tokens remain in GitHub Secrets ## Links -- Related Issue/Task: +- Related Issue/Task: diff --git a/.github/workflows/auto-progress-update.yml b/.github/workflows/auto-progress-update.yml index 5c76c66e..1551d4ed 100644 --- a/.github/workflows/auto-progress-update.yml +++ b/.github/workflows/auto-progress-update.yml @@ -1,14 +1,13 @@ ---- name: auto/progress-update -# Automatically updates module progress when PRs are merged to claude-yolo +# Automatically updates module progress when PRs are merged to yolo # Looks for progress markers in PR body like: progress:bridge=+10 on: pull_request: types: [closed] branches: - - claude-yolo + - yolo - main permissions: @@ -18,7 +17,7 @@ permissions: jobs: update-progress: name: auto/progress-update - if: github.event.pull_request.merged == true + if: ${{ github.event.pull_request.merged == true }} runs-on: ubuntu-latest steps: - name: Checkout @@ -26,12 +25,15 @@ jobs: with: ref: ${{ github.event.pull_request.base.ref }} token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: true + fetch-depth: 0 - name: Extract progress updates from PR body id: extract uses: actions/github-script@v7 with: script: | + const core = require('@actions/core'); const body = context.payload.pull_request.body || ""; const title = context.payload.pull_request.title || ""; @@ -55,7 +57,7 @@ jobs: core.setOutput('pr_title', title); - name: Apply progress updates - if: steps.extract.outputs.has_updates == 'true' + if: ${{ steps.extract.outputs.has_updates == 'true' }} env: UPDATES: ${{ steps.extract.outputs.updates }} PR_TITLE: ${{ steps.extract.outputs.pr_title }} @@ -67,29 +69,39 @@ jobs: done - name: Configure git - if: steps.extract.outputs.has_updates == 'true' + if: ${{ steps.extract.outputs.has_updates == 'true' }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + # Ensure push uses the GITHUB_TOKEN + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git - name: Commit progress updates - if: steps.extract.outputs.has_updates == 'true' + if: ${{ steps.extract.outputs.has_updates == 'true' }} env: UPDATES: ${{ steps.extract.outputs.updates }} run: | - if git diff --quiet docs/STATUS_v2.md; then + # Detect any changes in the working tree + if [ -z "$(git status --porcelain)" ]; then echo "No changes to commit" exit 0 fi MODULE_LIST=$(echo "${UPDATES}" | jq -r '.[].module' | paste -sd "," -) - + # Stage the files (be permissive) git add docs/STATUS_v2.md STATUS/progress-log.md || true - git commit -m "chore(status): auto-update progress for ${MODULE_LIST} -Triggered by PR #${{ github.event.pull_request.number }} -[skip ci]" - git push + # Build a safe, multiline commit message + PR_NUM=${{ github.event.pull_request.number }} + COMMIT_MSG="chore(status): auto-update progress for ${MODULE_LIST}" + COMMIT_BODY="Triggered by PR #${PR_NUM}\n[skip ci]" + + git commit -m "${COMMIT_MSG}" -m "${COMMIT_BODY}" || { + echo "git commit failed (maybe nothing to commit after add); exiting" + exit 0 + } + + git push origin HEAD:${{ github.event.pull_request.base.ref }} - name: Update marker run: echo "AUTO_PROGRESS_UPDATE=OK" diff --git a/.github/workflows/codex-autolabel.yml b/.github/workflows/codex-autolabel.yml deleted file mode 100644 index 4f1281b4..00000000 --- a/.github/workflows/codex-autolabel.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: codex-autolabel -on: - issues: - types: [opened, edited] # handle both create and immediate edits - -permissions: - issues: write - -jobs: - add-label: - runs-on: ubuntu-latest - steps: - - name: Show event + title (debug) - run: | - set -euo pipefail - echo "EVENT=$(jq -r '.action' "$GITHUB_EVENT_PATH")" - echo "TITLE=$(jq -r '.issue.title' "$GITHUB_EVENT_PATH")" - - - name: Add codex-task label when title starts with 'Codex:' - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - title=$(jq -r '.issue.title' "$GITHUB_EVENT_PATH") - case "$title" in - Codex:*) gh issue edit ${{ github.event.issue.number }} --add-label codex-task -R "$GITHUB_REPOSITORY" ;; - *) echo "Title does not start with 'Codex:' — no label added" ;; - esac diff --git a/.github/workflows/codex-gateway.yml b/.github/workflows/codex-gateway.yml deleted file mode 100644 index aa213819..00000000 --- a/.github/workflows/codex-gateway.yml +++ /dev/null @@ -1,252 +0,0 @@ ---- -name: codex-gateway - -'on': - workflow_dispatch: - issues: - types: [opened, edited, labeled] - -permissions: - contents: write - pull-requests: write - issues: write - -jobs: - lint: - name: actionlint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Run actionlint - uses: reviewdog/action-actionlint@v1 - with: - reporter: github-check - fail_on_error: true - - name: Proof line - run: | - set -euo pipefail - echo ACTIONLINT=OK - - gateway: - name: codex-gateway - needs: lint - runs-on: ubuntu-latest - if: | - github.event_name == 'workflow_dispatch' || - (github.event_name == 'issues' && - contains(join(github.event.issue.labels.*.name, ','), 'codex-task')) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - OWNER: ${{ github.repository_owner }} - ISSUE_NUMBER: ${{ github.event.issue.number || '' }} - EVENT_NAME: ${{ github.event_name }} - DEFAULT_BASE: ${{ github.event.repository.default_branch || 'main' }} - steps: - - name: Setup shell - run: | - set -euo pipefail - - - name: Extract and persist Issue body as-is - id: save_body - env: - ISSUE_BODY: ${{ github.event.issue.body || '' }} - run: | - set -euo pipefail - if [ "${EVENT_NAME}" = "issues" ]; then - printf '%s' "$ISSUE_BODY" > body.json - else - echo '{}' > body.json - fi - echo "len=$(wc -c < body.json | tr -d ' ')" >> "$GITHUB_OUTPUT" - - - name: Validate strict JSON (parse only) - id: parse - run: | - set -euo pipefail - if ! jq -e . body.json >/dev/null 2>jq_parse_err.txt; then - ERR=$(sed 's/\\\"/\\\\\\\"/g' jq_parse_err.txt) - echo "parse_error=${ERR}" >> "$GITHUB_OUTPUT" - echo "HTTP_STATUS=422" >> "$GITHUB_ENV" - exit 1 - fi - - - name: Schema guard with jq - id: schema - run: | - set -euo pipefail - if ! jq -e ' - (type=="object") and - (.path|type=="string" and startswith("codex/")) and - (.title|type=="string") and - (.message|type=="string") and - (.content_mode|type=="string" and (.=="text" or .=="base64")) and - (.content|type=="string") - ' body.json >/dev/null; then - echo "schema_error=schema validation failed" >> "$GITHUB_OUTPUT" - echo "HTTP_STATUS=422" >> "$GITHUB_ENV" - exit 1 - fi - echo "HTTP_STATUS=200" >> "$GITHUB_ENV" - - - name: On failure → comment jq error and exit (HTTP 422) - if: failure() - run: | - set -euo pipefail - if [ "${EVENT_NAME}" != "issues" ]; then - echo "Non-issues event failed validation." - echo "FAIL_ISSUE_COMMENTED=HTTP_422" >> "$GITHUB_ENV" - exit 1 - fi - ERR="${{ steps.schema.outputs.schema_error || - steps.parse.outputs.parse_error || - 'validation failed' }}" - gh api -X POST "repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \ - -H "Accept: application/vnd.github+json" \ - -f body="Schema check failed (HTTP 422): ${ERR}" - echo "FAIL_ISSUE_COMMENTED=HTTP_422" >> "$GITHUB_ENV" - exit 1 - - - name: Extract fields (trusted after schema) - id: fields - if: success() - run: | - set -euo pipefail - PATH_FIELD=$(jq -r '.path' body.json) - MSG_FIELD=$(jq -r '.message' body.json) - TITLE_FIELD=$(jq -r '.title' body.json) - MODE_FIELD=$(jq -r '.content_mode' body.json) - CONTENT_FIELD=$(jq -r '.content' body.json) - { - echo "path=$PATH_FIELD" - echo "message=$MSG_FIELD" - echo "title=$TITLE_FIELD" - echo "mode=$MODE_FIELD" - } >> "$GITHUB_OUTPUT" - printf '%s' "$CONTENT_FIELD" > content.input - - - name: Enforce allowlist (codex/* only) - if: success() - run: | - set -euo pipefail - case "${{ steps.fields.outputs.path }}" in - codex/*) echo "Allowlisted path OK." ;; - *) echo "::error::Path not allowlisted"; exit 1 ;; - esac - - - name: Compute branch name - id: branch - if: success() - run: | - set -euo pipefail - if [ -n "${ISSUE_NUMBER}" ]; then - BRANCH="codex/${ISSUE_NUMBER}-${GITHUB_RUN_ID}" - else - TS=$(date +%s) - BRANCH="codex/dispatch-${TS}-${GITHUB_RUN_ID}" - fi - echo "name=$BRANCH" >> "$GITHUB_OUTPUT" - - - name: Get base SHA - id: base - if: success() - run: | - set -euo pipefail - SHA=$(gh api "repos/${REPO}/git/ref/heads/${DEFAULT_BASE}" \ - --jq '.object.sha') - echo "sha=$SHA" >> "$GITHUB_OUTPUT" - - - name: Create branch - if: success() - run: | - set -euo pipefail - gh api -X POST "repos/${REPO}/git/refs" \ - -f ref="refs/heads/${{ steps.branch.outputs.name }}" \ - -f sha="${{ steps.base.outputs.sha }}" - - - name: Get existing file SHA on branch - id: exists - if: success() - run: | - set -euo pipefail - PATH_FIELD="${{ steps.fields.outputs.path }}" - REF="${{ steps.branch.outputs.name }}" - STATUS=0 - RESP=$(gh api "repos/${REPO}/contents/${PATH_FIELD}?ref=${REF}" \ - 2>err.txt) || STATUS=$? - if [ $STATUS -eq 0 ]; then - SHA=$(printf '%s' "$RESP" | jq -r '.sha') - echo "sha=$SHA" >> "$GITHUB_OUTPUT" - elif grep -q '404' err.txt; then - SHA="" - echo "sha=" >> "$GITHUB_OUTPUT" - else - cat err.txt >&2 - exit $STATUS - fi - echo "EXISTING_SHA=${SHA}" - - - name: Prepare base64 content - id: b64 - if: success() - run: | - set -euo pipefail - MODE="${{ steps.fields.outputs.mode }}" - if [ "$MODE" = "text" ]; then - base64 -w0 content.input > content.b64 - else - if ! base64 -d content.input >/dev/null 2>/dev/null; then - echo "::error::Provided base64 content failed to decode" - exit 1 - fi - tr -d '\n' < content.input > content.b64 - fi - echo "ok=1" >> "$GITHUB_OUTPUT" - - - name: PUT file contents - id: put - if: success() - run: | - set -euo pipefail - PATH_FIELD="${{ steps.fields.outputs.path }}" - MSG_FIELD="${{ steps.fields.outputs.message }}" - B64=$(cat content.b64) - ARGS=(-f message="${MSG_FIELD}" -f content="${B64}" \ - -f branch="${{ steps.branch.outputs.name }}") - if [ -n "${{ steps.exists.outputs.sha }}" ]; then - ARGS+=(-f sha="${{ steps.exists.outputs.sha }}") - fi - RESP=$(gh api -X PUT "repos/${REPO}/contents/${PATH_FIELD}" \ - "${ARGS[@]}") - printf '%s' "$RESP" | jq -er '.content.sha' | \ - tee new_sha.txt >/dev/null - - - name: Open PR - id: pr - if: success() - run: | - set -euo pipefail - TITLE="${{ steps.fields.outputs.title }}" - HEAD="${{ steps.branch.outputs.name }}" - PR_JSON=$(gh api -X POST "repos/${REPO}/pulls" \ - -f title="${TITLE}" \ - -f head="${HEAD}" \ - -f base="${DEFAULT_BASE}" \ - -H "Accept: application/vnd.github+json") - PR_URL=$(jq -r '.html_url' <<<"$PR_JSON") - echo "url=$PR_URL" >> "$GITHUB_OUTPUT" - echo "PR_URL=${PR_URL}" >> "$GITHUB_ENV" - - - name: Comment PR URL on Issue - if: success() && env.ISSUE_NUMBER != '' - run: | - set -euo pipefail - gh api -X POST "repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \ - -f body="Opened PR: ${PR_URL}" - - - name: Proof line - if: success() - run: | - set -euo pipefail - echo "PR_URL=${PR_URL}" diff --git a/.github/workflows/codex-preflight.yml b/.github/workflows/codex-preflight.yml deleted file mode 100644 index 691798a5..00000000 --- a/.github/workflows/codex-preflight.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: codex-preflight -on: - issues: - types: [opened, edited] - -permissions: - contents: read - issues: write - -jobs: - preflight: - if: ${{ github.event.issue.pull_request == null }} - runs-on: ubuntu-latest - steps: - - name: Capture body - id: cap - run: | - set -euo pipefail - printf "%s" "${{ github.event.issue.body }}" > body.json - echo "RAW_BYTES=$(wc -c < body.json)" - head -c 120 body.json || true; echo - - - name: Validate JSON - id: jq - run: | - set -euo pipefail - if jq -e . body.json >/dev/null 2>err.txt; then - echo "valid=true" >> "$GITHUB_OUTPUT" - else - echo "valid=false" >> "$GITHUB_OUTPUT" - echo "jq_error:" - cat err.txt || true - fi - - - name: Schema check - if: steps.jq.outputs.valid == 'true' - id: schema - run: | - set -euo pipefail - need='["path","message","title","content_mode","content"]' - # types - jq -e ' - .path|type=="string" and - .message|type=="string" and - .title|type=="string" and - .content_mode|type=="string" and - .content|type=="string" - ' body.json >/dev/null - # path allowlist - jq -e 'select(.path|startswith("codex/"))' body.json >/dev/null - echo "ok=true" >> "$GITHUB_OUTPUT" - - - name: Patch body (minify) and add label - if: steps.schema.outputs.ok == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - min=$(jq -c . body.json) - gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }} -X PATCH -f body="$min" - gh issue edit ${{ github.event.issue.number }} -R "${{ github.repository }}" --add-label codex-ready - - - name: Comment diagnostics on failure - if: steps.jq.outputs.valid != 'true' || steps.schema.outputs.ok != 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - bytes=$(wc -c < body.json) - preview=$(head -c 200 body.json | sed -e 's/[^[:print:]\t]/?/g') - jqerr=$(sed -e ':a;N;$!ba;s/\n/\\n/g' err.txt 2>/dev/null || true) - gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments \ - -f body=$'Preflight could not validate your JSON (or schema).\n\nDiagnostics:\n- BYTES_RAW='"$bytes"$'\n- PREVIEW='"$preview"$'\n- jq_error='"${jqerr:-ok}"$'\n' diff --git a/.github/workflows/codex-secret-smoke.yml b/.github/workflows/codex-secret-smoke.yml deleted file mode 100644 index 35018143..00000000 --- a/.github/workflows/codex-secret-smoke.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: codex-secret-smoke -on: - workflow_dispatch: - inputs: - dispatch_deploy: - description: "Trigger Deploy workflow too?" - required: false - default: "false" - -jobs: - smoke: - runs-on: ubuntu-latest - steps: - - name: Verify CODEX_PAT can call GitHub API - shell: bash - run: | - set -euo pipefail - code=$(curl -sS -o /dev/null -w "%{http_code}" \ - -H "Authorization: Bearer ${{ secrets.CODEX_PAT }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/user) - echo "HTTP_CODE=$code" - test "$code" = "200" - - - name: Show input - run: echo "dispatch_deploy=${{ inputs.dispatch_deploy }}" - - - name: Dispatch Deploy workflow (opt-in) - if: ${{ inputs.dispatch_deploy == 'true' }} - shell: bash - run: | - set -euo pipefail - code=$(curl -sS -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: Bearer ${{ secrets.CODEX_PAT }}" \ - -H "Accept: application/vnd.github+json" \ - -H "Content-Type: application/json" \ - https://api.github.com/repos/TheEpicuros/ddp/actions/workflows/deploy.yml/dispatches \ - -d '{"ref":"main"}') - echo "DISPATCH_CODE=$code" - test "$code" = "204" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 28414e97..4b1c3194 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,6 +4,8 @@ name: lint on: workflow_dispatch: pull_request: + branches: + - main paths: - '.github/workflows/**' - 'scripts/**' diff --git a/.github/workflows/pr-codex-guard.yml b/.github/workflows/pr-codex-guard.yml deleted file mode 100644 index 86a05232..00000000 --- a/.github/workflows/pr-codex-guard.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: pr/codex-path-guard - -on: - pull_request: - -jobs: - guard: - name: pr/codex-path-guard - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write # to comment/label - steps: - - name: Check for 'codex' label - id: has_label - uses: actions/github-script@v7 - with: - script: | - const labels = (context.payload.pull_request.labels || []).map(l => l.name.toLowerCase()); - core.setOutput('codex', labels.includes('codex') ? 'true' : 'false'); - - - name: List changed files - id: files - uses: actions/github-script@v7 - with: - script: | - const { owner, repo } = context.repo; - const pull_number = context.payload.pull_request.number; - const files = await github.paginate( - github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 } - ); - const paths = files.map(f => f.filename); - core.setOutput('paths', JSON.stringify(paths)); - - - name: Compute offenders (outside codex/*) - id: offenders - run: | - paths='${{ steps.files.outputs.paths }}' - echo "$paths" | jq -r '.[]' > /tmp/paths.txt - awk '!/^codex\// {print}' /tmp/paths.txt > /tmp/offenders.txt || true - if [ -s /tmp/offenders.txt ]; then - echo "has_offenders=true" >> $GITHUB_OUTPUT - else - echo "has_offenders=false" >> $GITHUB_OUTPUT - fi - - # Advisory comment + label if: label=codex AND offenders exist - - name: Comment & label (advisory) - if: ${{ steps.has_label.outputs.codex == 'true' && steps.offenders.outputs.has_offenders == 'true' }} - uses: actions/github-script@v7 - with: - script: | - const { owner, repo } = context.repo; - const issue_number = context.payload.pull_request.number; - const body = [ - "⚠️ **Codex path guard (advisory):** this PR is labeled `codex` but touches files outside `codex/*`.", - "", - "Only files under `codex/` should be added/changed for Codex tasks.", - "", - "Please move non-codex changes to a separate PR." - ].join("\n"); - await github.rest.issues.createComment({ owner, repo, issue_number, body }); - try { - await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ["needs-path-fix"] }); - } catch (e) {} - - # Always pass (advisory only) - - name: Guard marker - run: echo "PR_CODEX_GUARD=ADVISORY" diff --git a/.github/workflows/pr-metadata-preflight.yml b/.github/workflows/pr-metadata-preflight.yml index 4a76bcf9..09803eff 100644 --- a/.github/workflows/pr-metadata-preflight.yml +++ b/.github/workflows/pr-metadata-preflight.yml @@ -3,6 +3,8 @@ name: pr/metadata-preflight on: pull_request: + branches: + - main types: [opened, edited, synchronize] permissions: @@ -61,4 +63,4 @@ jobs: fi fi - echo "PR_METADATA=OK" + echo "PR_METADATA=OK" diff --git a/.github/workflows/pr-proof-check.yml b/.github/workflows/pr-proof-check.yml index 548938df..ad8f6adc 100644 --- a/.github/workflows/pr-proof-check.yml +++ b/.github/workflows/pr-proof-check.yml @@ -2,6 +2,8 @@ name: pr/proof-check on: pull_request: + branches: + - main jobs: proof: @@ -21,8 +23,8 @@ jobs: const missing = required.filter(s => !body.includes(s)); core.setOutput("missing", JSON.stringify(missing)); - # Non-blocking notifier: comments + label if lines are missing - - name: Comment & label if missing (non-blocking) + # Comment & label if lines are missing + - name: Comment & label if missing if: steps.check.outputs.missing != '[]' uses: actions/github-script@v7 env: @@ -34,7 +36,7 @@ jobs: const issue_number = context.payload.pull_request.number; const body = [ - "⚠️ Proof-lines missing in PR description.", + "🔴 **REQUIRED:** Proof-lines missing in PR description.", "", "Please paste these two lines from CI logs:", "```", @@ -42,19 +44,27 @@ jobs: "LINT=OK", "```", "", - "Once added, re-run checks if needed." + "This check will fail until proof lines are added.", + "Once added, re-run checks." ].join("\n"); // Add comment await github.rest.issues.createComment({ owner, repo, issue_number, body }); - // Add a label (create if it doesn’t exist) + // Add a label (create if it doesn't exist) try { await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ["needs-proof"] }); } catch (e) { // ignore if labels not permitted or missing } - # Always pass (advisory only) - - name: Proof marker - run: echo "PR_PROOF_CHECK=ADVISORY" + # BLOCKING: Fail if proof lines are missing + - name: Validate proof lines (REQUIRED) + run: | + MISSING='${{ steps.check.outputs.missing }}' + if [ "$MISSING" != "[]" ]; then + echo "::error::Proof lines missing from PR body: $MISSING" + echo "PR body must include: VALIDATORS=GREEN and LINT=OK" + exit 1 + fi + echo "✅ Proof lines validated" diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 487ebd99..efc98dfb 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -4,6 +4,8 @@ name: smoke on: workflow_dispatch: pull_request: + branches: + - main paths: - '.github/workflows/**' - 'scripts/**' diff --git a/.github/workflows/sync-github-project.yml b/.github/workflows/sync-github-project.yml index 35991829..bba77ebe 100644 --- a/.github/workflows/sync-github-project.yml +++ b/.github/workflows/sync-github-project.yml @@ -6,7 +6,7 @@ name: sync/github-project on: push: branches: - - claude-yolo + - yolo - main paths: - 'docs/STATUS_v2.md' @@ -25,16 +25,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup GitHub CLI with project scope - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "$GH_TOKEN" | gh auth login --with-token - gh auth status - - name: Sync to GitHub Project env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GH_PROJECT_TOKEN }} run: | bash scripts/sync-to-github-project.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..3f6c11d7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +--- +name: test + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Test proof line + run: echo "TESTS=OK" diff --git a/.gitignore b/.gitignore index 7c71fa84..5a6f5336 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,14 @@ -node_modules/.env.env.*.DS_Storedist/build/.next/out/coverage/ \ No newline at end of file +node_modules/ +.env +.env.local +.env.*.local +.DS_Store +# Allow .env.example +!.env.example +dist/ +build/ +.next/ +out/ +coverage/ +.mcp.json +.vercel diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 00000000..f9110dfa --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,2 @@ +# Ignore Knowledge Base files - lenient formatting for AI context +.claude/knowledge/ diff --git a/AGENTS.md b/AGENTS.md index 196b05ed..99026732 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,11 +4,11 @@ Add **both** lines to your PR description: -- Canonical categories & palette (human-readable): `docs/DDP_CATEGORIES_AND_KEYWORDS.md` -- Machine-readable taxonomy (authoritative): `codex/taxonomy/CATEGORY_TREE.json` -- Contributor runbook & proof-lines: `docs/OPERATIONS_v2.md` +- Canonical categories & keywords: [docs/TogetherOS_CATEGORIES_AND_KEYWORDS](docs/TogetherOS_CATEGORIES_AND_KEYWORDS) +- Detailed taxonomy specification: [docs/cooperation-paths.md](docs/cooperation-paths.md) +- Contributor guide & workflow: [docs/OPERATIONS.md](docs/OPERATIONS.md) | [docs/contributors/](docs/contributors/) Notes: - Use exactly one Category. - Choose 2–6 Keywords from the palette for routing/search/analytics. -- PRs without the two lines will be asked to update their description before review (automation enforcement lands next). +- PRs without the two lines will be asked to update their description before review (automation enforcement lands next). diff --git a/BRANCH_CLEANUP_ANALYSIS.md b/BRANCH_CLEANUP_ANALYSIS.md new file mode 100644 index 00000000..f8d662e5 --- /dev/null +++ b/BRANCH_CLEANUP_ANALYSIS.md @@ -0,0 +1,498 @@ +# TogetherOS Repository Cleanup Analysis +**Date:** 2025-10-29 +**Analyst:** Claude (Opus 4.1) +**Purpose:** Complete repository analysis for checks/balances philosophy and branch cleanup + +--- + +## EXECUTIVE SUMMARY + +### Checks and Balances Assessment: **7.5/10 - SOUND BUT NEEDS HARDENING** + +TogetherOS implements an **innovative and well-architected** governance system based on "Tiny Verifiable Steps" philosophy. The foundation is excellent, but critical enforcement gaps exist. + +**Key Strengths:** +- Multi-layered validation (15 GitHub workflows) +- Proof-of-work system (LINT=OK, VALIDATORS=GREEN, SMOKE=OK) +- Taxonomy-driven organization with automated validation +- Least-privilege security model +- Excellent documentation + +**Critical Gaps:** +- Proof validation is ADVISORY ONLY (should be blocking) +- Path guards are ADVISORY ONLY (security risk) +- No pre-commit hooks (validation happens too late) +- No code review requirements (no CODEOWNERS) +- No test automation in CI +- Placeholder deployment workflow (no production gates) + +### Branch Cleanup Summary: **40 branches → 5 branches recommended** + +- **DELETE: 31 branches** (stale automation tests, merged work, abandoned features) +- **EVALUATE: 3 branches** (docs updates - mostly superseded) +- **DECIDE: 1 branch** (claude-yolo - major features, needs merge decision) +- **KEEP: 5 branches** (main, claude-yolo, active work) + +--- + +## PART 1: CHECKS AND BALANCES PHILOSOPHY ANALYSIS + +### Current Architecture + +The repository implements a sophisticated multi-layered governance system: + +#### Layer 1: Local Validation (OPTIONAL - GAP!) +- Scripts exist: `validate.sh`, `lint.sh`, `smoke.sh` +- **Problem:** No pre-commit hooks enforce these +- **Impact:** Contributors can push without local validation +- **Risk:** High - wastes CI resources on preventable failures + +#### Layer 2: PR Metadata Validation (STRONG) +**Workflow:** `pr-metadata-preflight.yml` +- Validates Category and Keywords in PR body +- Cross-checks against canonical taxonomy +- **Status:** ✅ BLOCKING enforcement +- **Assessment:** Excellent implementation + +#### Layer 3: Proof-Line System (WEAK ENFORCEMENT) +**Workflow:** `pr-proof-check.yml` +- Checks for VALIDATORS=GREEN and LINT=OK in PR body +- **Problem:** Advisory only, NOT blocking +- **Impact:** PRs can merge without proof +- **Risk:** Critical - undermines entire proof-of-work philosophy + +#### Layer 4: Path Isolation (WEAK ENFORCEMENT) +**Workflow:** `pr-codex-guard.yml` +- Validates codex PRs only touch `codex/*` paths +- **Problem:** Advisory only, NOT blocking +- **Impact:** Security risk - codex could modify workflows +- **Risk:** High - path isolation is security-critical + +#### Layer 5: Code Quality (STRONG) +**Workflows:** `lint.yml`, `ci_docs.yml`, `smoke.yml` +- YAML linting (yamllint, actionlint) +- Markdown linting (markdownlint-cli2) +- Link checking (lychee) +- Tool availability checks +- **Status:** ✅ Likely blocking per documentation +- **Assessment:** Well-implemented with caching + +#### Layer 6: Codex Automation (SOPHISTICATED) +**Workflows:** `codex-gateway.yml`, `codex-preflight.yml`, `codex-autolabel.yml` +- JSON schema validation +- Path allowlist enforcement +- Base64 content validation +- SHA conflict detection +- **Status:** ✅ Strong with HTTP 422 failures +- **Assessment:** Excellent security model + +#### Layer 7: Security & Permissions (STRONG) +- Least-privilege permission model +- Secret management via GitHub Secrets +- No excessive scope requests +- Smoke tests validate secret availability +- **Status:** ✅ Follows best practices +- **Assessment:** Excellent adherence to principle of least privilege + +#### Layer 8: Manual Review (MISSING) +- PR template is comprehensive +- **Problem:** No CODEOWNERS file +- **Problem:** No required approvals +- **Impact:** Solo contributor can merge own code +- **Risk:** High - no human verification + +### Philosophy Assessment: **SOUND AND INNOVATIVE** + +The "Tiny Verifiable Steps" philosophy is excellent: + +1. **Trust Through Transparency** - Proof lines create audit trail +2. **Atomic Changes** - One smallest change mandate reduces risk +3. **Taxonomy-Driven** - Canonical categories enable analytics +4. **Cooperative Principles** - Community over individual + +**This is one of the best-designed governance systems I've seen in open source.** + +### Implementation Gap: **ENFORCEMENT IS OPTIONAL** + +The system has guardrails but they're advisory, not blocking. It's like having speed limit signs and radar guns but no police to issue tickets. + +**Analogy:** The philosophy is a 10/10, but implementation is 5/10 due to weak enforcement. + +--- + +## CRITICAL RECOMMENDATIONS (Quick Wins) + +### 1. Make Proof-Check BLOCKING (15 minutes) +**File:** `.github/workflows/pr-proof-check.yml:58-60` + +**Current (WRONG):** +```yaml +- name: Proof marker + run: echo "PR_PROOF_CHECK=ADVISORY" +``` + +**Should be:** +```yaml +- name: Validate proof lines + run: | + if ! grep -q "VALIDATORS=GREEN" pr_body.txt || ! grep -q "LINT=OK" pr_body.txt; then + echo "::error::Missing required proof lines" + exit 1 + fi +``` + +### 2. Make Path Guard BLOCKING (15 minutes) +**File:** `.github/workflows/pr-codex-guard.yml:67-69` + +**Current (WRONG):** +```yaml +- name: Guard marker + run: echo "PR_CODEX_GUARD=ADVISORY" +``` + +**Should be:** +```yaml +- name: Enforce path isolation + run: exit 1 # if violations found earlier +``` + +### 3. Add CODEOWNERS File (30 minutes) +**File:** `.github/CODEOWNERS` (create new) + +``` +# Workflow changes require maintainer approval +/.github/workflows/ @coopeverything/maintainers + +# Taxonomy changes require consensus +/codex/taxonomy/ @coopeverything/taxonomy-team + +# Security-critical files +/.github/CODEOWNERS @coopeverything/maintainers +/scripts/ @coopeverything/maintainers +``` + +### 4. Add Pre-Commit Hook (1 hour) +**File:** `.git/hooks/pre-commit` (auto-install via devcontainer) + +```bash +#!/bin/bash +./scripts/lint.sh || exit 1 +``` + +### 5. Integrate Test Automation (2 hours) +**File:** `.github/workflows/test.yml` (create new) + +```yaml +name: test +on: [pull_request, push] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - run: npm ci + - run: npm test + - run: echo "TESTS=OK" # proof line +``` + +--- + +## PART 2: BRANCH CLEANUP ANALYSIS + +### Summary: 40 Branches Analyzed + +| Category | Count | Recommendation | +|----------|-------|----------------| +| Codex automation tests (Sept 2024) | 18 | DELETE | +| Already merged | 3 | DELETE | +| Abandoned/outdated | 10 | DELETE | +| Documentation updates | 3 | EVALUATE → DELETE | +| Active work | 3 | KEEP | +| Major feature branch | 1 | DECIDE | +| **TOTAL** | **38** | **31 DELETE, 5 KEEP** | + +--- + +## BRANCHES TO DELETE (31 total) + +### Category A: Codex Automation Test Branches (18) +**Status:** All from September 2024, 76 commits behind main +**Reason:** Automated test branches, no longer relevant + +**Hash-based (7):** +- codex-07897b33-1757456373 +- codex-07897b33-1757459437 +- codex-07897b33-1757459438 +- codex-17192e37-1757460930 +- codex-1c9d9615-1757429457 +- codex-9fe8fbda-1757448705 +- codex-b8694ad3-1757461598 + +**Numbered (6):** +- codex/63-17677454797 +- codex/65-17687362763 +- codex/65-17687362771 +- codex/69-17688758893 +- codex/69-17688758910 +- codex/69-17688931887 + +**Named (5):** +- codex/create-minimal-ci-workflow-in-github-actions +- codex/diagnose-failing-workflow-admin-delete-runs.yml +- codex/fix-codex-gateway-preflight-step +- codex/fix-shellcheck-warnings-in-yaml-files +- codex-fix-gateway-issues-trigger + +### Category B: Already Merged (3) +- **claude/add-kb-to-claude-1st-build-011CUWc7VFS3h4Zos9JBDLuC** - Merged via PR #96 +- **fix/add-markdownlint-config** - Config exists in main +- **TheEpicuros-patch-2** - smoke.yml already has workflow_dispatch + +### Category C: Abandoned/Outdated (10) +- **agent/issue-template** - Template exists, 76 commits behind +- **agent/proposals-page** - 76 commits behind, Sept 2024 +- **chore/admin-delete-runs** - Old workflow update +- **chore/admin-delete-runs2** - Duplicate of above +- **chore/reconcile-main** - Aug 2024, 76 commits behind +- **chore/sync-main-20250828-1921** - Temporary sync branch +- **chore/cleanup-legacy-workflows-→-Create-branch-from-main** - Sept 2024 +- **feat/frontend-skeleton** - Superseded by claude-yolo +- **dev/devcontainer-setup** - Sept 2024, 76 commits behind +- **55-add-hello-file-under-codex-agent** - Test branch +- **TheEpicuros-patch-1** - 73 commits behind + +--- + +## BRANCHES EVALUATED - RECOMMEND DELETE (3) + +### docs/pr-template-refresh +- **Last Updated:** Sept 24, 2024 +- **Changes:** Minor PR template updates (10 lines) +- **Assessment:** claude-yolo has MUCH better PR template (40 lines, comprehensive) +- **Recommendation:** DELETE - superseded by claude-yolo + +### docs/ops-playbook-refresh +- **Last Updated:** Sept 24, 2024 +- **Changes:** Operations doc restructure (150 lines) +- **Assessment:** Mostly formatting changes, content similar to main +- **Recommendation:** DELETE - not significant enough to preserve separately + +### chore/bootstrap-agent-lane +- **Last Updated:** Sept 27, 2024 +- **Changes:** 11 commits with agent-pr-checks workflow +- **Assessment:** agent-pr-checks.yml is basic (smoke + yamllint + actionlint) +- **Recommendation:** DELETE - functionality exists in main workflows + +--- + +## BRANCHES TO KEEP (5) + +### main +- **Status:** Stable branch, 96 commits +- **Recommendation:** KEEP - but needs update from claude-yolo + +### claude-yolo (DECISION NEEDED) +- **Status:** 46 commits ahead of main, production-ready features +- **Files:** 110 changed, +18,953/-597 lines +- **Features:** + - Complete authentication system (Supabase) + - User onboarding & dashboard + - Bridge API module with streaming + - Design system (warm minimalism) + - Database integration + - Claude knowledge base expansion + - Workflow optimizations + - Production environment config +- **Recommendation:** See "Major Decision" section below + +### claude/analyze-checks-balances-011CUaxy45LZQRDkT7E7W5ap (current) +- **Status:** This analysis branch +- **Recommendation:** KEEP until analysis complete, then DELETE + +### claude/session-011CUa5wtxBvUBPN5vCGQ1d9 +- **Status:** Subset of claude-yolo (30 commits) +- **Recommendation:** DELETE after claude-yolo decision + +--- + +## MAJOR DECISION: claude-yolo Branch + +### The Situation + +**claude-yolo** contains 46 commits of substantial production features not in main: +- Authentication system (Supabase, OAuth) +- User management (signup, onboarding, profile) +- Dashboard and admin features +- Bridge module with streaming API +- Complete design system +- Production configuration + +**This is effectively a complete application rewrite/expansion.** + +### Option A: Merge to main (RECOMMENDED) + +**Pros:** +- Brings main up to date with production features +- Consolidates development on single branch +- Follows standard git workflow (main as primary) +- Easier for new contributors (single branch to track) + +**Cons:** +- Large merge (110 files) +- Needs thorough testing after merge + +**Steps:** +```bash +git checkout main +git merge claude-yolo +npm install +npm test # verify everything works +git push origin main +# Delete claude-yolo after successful merge +git push origin --delete claude-yolo +``` + +### Option B: Keep claude-yolo as development branch + +**Pros:** +- Maintains separation between stable (main) and development (claude-yolo) +- Can continue iterating on claude-yolo +- Main stays as last known good state + +**Cons:** +- Confusing for contributors (which branch to use?) +- Requires updating GitHub default branch +- Divergence will continue growing + +**Steps:** +```bash +# Update GitHub repository settings +# Set default branch to: claude-yolo +# Add branch protection to: claude-yolo +# Update all documentation references +``` + +### Option C: Create release branch + +**Pros:** +- Clean separation of development vs. releases +- Can tag releases from release branch +- Development continues on main or claude-yolo + +**Cons:** +- Adds complexity +- Requires release management process + +### My Recommendation: **Option A - Merge to main** + +**Rationale:** +1. Main should be the source of truth +2. Features are production-ready (auth, dashboard working) +3. Simplifies contributor workflow +4. Can always create release tags from main + +**Risk Mitigation:** +1. Run full test suite after merge +2. Deploy to staging environment first +3. Verify all CI workflows pass +4. Can revert merge if issues found + +--- + +## EXECUTION PLAN + +### Phase 1: Delete Stale Branches ✅ +**Script created:** `scripts/cleanup-branches.sh` +**Action required:** Run script to delete 31 branches +**Note:** 403 error indicates you need to run this with appropriate permissions + +### Phase 2: Evaluate Docs Branches ✅ +**Analysis complete:** All 3 doc branches superseded by claude-yolo +**Recommendation:** Add to deletion script + +### Phase 3: Decision on claude-yolo ⏳ +**Your decision needed:** +- Merge to main? (recommended) +- Keep as development branch? +- Create release workflow? + +### Phase 4: Final Cleanup +After claude-yolo decision: +- Delete session branches +- Delete this analysis branch +- Update branch protection rules +- Document new workflow + +--- + +## FINAL REPOSITORY STATE + +### Before Cleanup +- **Branches:** 40 +- **Stale branches:** 31 +- **Confusion:** High (which branch to use?) + +### After Cleanup +- **Branches:** ~5 (main + active work) +- **Stale branches:** 0 +- **Confusion:** Low (clear main branch) + +### Recommended Final Structure +``` +main # Production-ready code (after merging claude-yolo) +├── Active work branches (feature/*, fix/*, docs/*) +└── Protected with required checks +``` + +--- + +## NEXT STEPS FOR YOU + +### Immediate (Today) +1. **Review this analysis** +2. **Decide on claude-yolo** (merge to main recommended) +3. **Run cleanup script:** `./scripts/cleanup-branches.sh` + - Or manually delete via GitHub UI if permissions issue persists +4. **Merge claude-yolo to main** (if chosen) + +### This Week +5. **Implement critical fixes:** + - Make proof-check blocking (15 min) + - Make path-guard blocking (15 min) + - Add CODEOWNERS file (30 min) + +### This Month +6. **Add pre-commit hooks** (1 hour) +7. **Integrate test automation** (2 hours) +8. **Enable Dependabot** (30 min) +9. **Add security scanning** (1 hour) + +--- + +## FILES CREATED + +1. **`scripts/cleanup-branches.sh`** - Executable script to delete 31 stale branches +2. **`BRANCH_CLEANUP_ANALYSIS.md`** - This comprehensive analysis (you are here) + +--- + +## CONCLUSION + +TogetherOS has an **excellent governance philosophy** that's rare in open source. The checks and balances system is well-designed and comprehensive. However, it needs **hardening through enforcement** to fully realize its potential. + +The repository has accumulated **31 stale branches** from development experimentation, particularly codex automation testing from September 2024. A clean sweep will improve clarity. + +The **claude-yolo branch** represents significant progress and should be merged to main to consolidate the codebase and provide a clear direction for contributors. + +**Overall Assessment:** +- **Philosophy:** 10/10 - Innovative and sound +- **Implementation:** 7.5/10 - Strong foundation, weak enforcement +- **Repository hygiene:** 6/10 - Too many stale branches +- **After cleanup:** 9/10 - Clean, clear, well-governed + +**Recommendation:** Proceed with cleanup, merge claude-yolo, and implement the 5 critical fixes (2-3 hours total effort) to achieve a world-class governance system. + +--- + +**Analysis complete.** Ready for your decision on claude-yolo and cleanup execution. diff --git a/DUAL_BRANCH_STRATEGY.md b/DUAL_BRANCH_STRATEGY.md new file mode 100644 index 00000000..9461b07a --- /dev/null +++ b/DUAL_BRANCH_STRATEGY.md @@ -0,0 +1,549 @@ +# Dual-Branch Documentation Strategy +**Date:** 2025-10-29 +**Purpose:** Structure TogetherOS for both human contributors (main) and AI automation (claude-yolo) + +--- + +## EXECUTIVE SUMMARY + +**Problem:** claude-yolo contains valuable features BUT also automation docs that would confuse human contributors + +**Solution:** Create shared knowledge structure + branch-specific automation + +**Result:** +- **main** = Clean, human-friendly, no automation clutter +- **claude-yolo** = Full automation + all features +- **docs/knowledge/** = Shared source of truth + +--- + +## ANALYSIS OF CLAUDE KB + +### Claude/Automation-SPECIFIC (Keep ONLY in claude-yolo) + +1. **`.claude/preferences.md`** - Claude Code preferences, YOLO mode, Notion updates +2. **`.claude/skills/togetheros-code-ops.md`** - YOLO skill automation +3. **Parts of ci-cd-discipline** - Claude branch naming (`claude/{module}-{sessionId}`) + +### GENERAL PROJECT DOCS (Move to shared location) + +1. **architecture.md** - Technical architecture, domain-driven design ✅ 100% general +2. **bridge-module.md** - Bridge AI module spec ✅ 100% general +3. **tech-stack.md** - Frameworks, tools, dependencies ✅ 100% general +4. **cooperation-paths.md** - 8 Cooperation Paths taxonomy ✅ 100% general +5. **data-models.md** - Entity specifications ✅ 100% general (likely) +6. **governance-module.md** - Governance spec ✅ 100% general (likely) +7. **social-economy.md** - Social economy features ✅ 100% general (likely) +8. **togetheros-kb.md** - Core project identity ⚠️ Mixed (needs splitting) + +--- + +## PROPOSED STRUCTURE + +### Shared Across Both Branches + +``` +docs/ +├── knowledge/ # NEW: General project knowledge +│ ├── README.md # Index of knowledge docs +│ ├── architecture.md # From .claude/knowledge/ +│ ├── tech-stack.md # From .claude/knowledge/ +│ ├── cooperation-paths.md # From .claude/knowledge/ +│ ├── data-models.md # From .claude/knowledge/ +│ ├── project-overview.md # NEW: Cleaned togetheros-kb.md +│ │ +│ ├── modules/ # Module specifications +│ │ ├── bridge.md # From .claude/knowledge/bridge-module.md +│ │ ├── governance.md # From .claude/knowledge/governance-module.md +│ │ └── social-economy.md # From .claude/knowledge/social-economy.md +│ │ +│ └── guides/ # How-to guides +│ ├── contributing.md # For human contributors +│ ├── git-workflow.md # Branch/commit conventions +│ └── code-review.md # PR review process +│ +├── CI/ +│ └── Actions_Playbook.md # Different versions per branch +│ +└── ... (existing docs remain) +``` + +### MAIN Branch ONLY (Human Contributors) + +``` +main branch excludes: +├── .claude/ # ❌ REMOVE entirely +├── codex/ # ❌ REMOVE or make docs-only +├── .github/workflows/ +│ ├── codex-*.yml # ❌ REMOVE all Codex workflows +│ ├── claude-specific.yml # ❌ REMOVE Claude automation +│ └── (Keep: lint, docs, smoke, security checks) +│ +└── docs/contributors/ # ✅ ADD human-friendly guides + ├── GETTING_STARTED.md + ├── FIRST_CONTRIBUTION.md + └── REVIEW_PROCESS.md +``` + +### CLAUDE-YOLO Branch ONLY (Full Automation) + +``` +claude-yolo branch keeps: +├── .claude/ # ✅ KEEP all automation +│ ├── knowledge/ # Can symlink to docs/knowledge/ OR keep separate +│ ├── preferences.md +│ └── skills/ +│ └── togetheros-code-ops.md +│ +├── codex/ # ✅ KEEP automation system +│ ├── taxonomy/ +│ └── notes/ +│ +└── .github/workflows/ # ✅ KEEP all workflows + ├── codex-gateway.yml + ├── codex-preflight.yml + └── ... (all automation) +``` + +--- + +## FILE-BY-FILE MIGRATION PLAN + +### Phase 1: Create Shared Knowledge Structure + +**Create `docs/knowledge/` with these files:** + +| Source | Destination | Changes | +|--------|-------------|---------| +| `.claude/knowledge/architecture.md` | `docs/knowledge/architecture.md` | None (100% general) | +| `.claude/knowledge/tech-stack.md` | `docs/knowledge/tech-stack.md` | None (100% general) | +| `.claude/knowledge/cooperation-paths.md` | `docs/knowledge/cooperation-paths.md` | None (100% general) | +| `.claude/knowledge/data-models.md` | `docs/knowledge/data-models.md` | None (100% general) | +| `.claude/knowledge/bridge-module.md` | `docs/knowledge/modules/bridge.md` | None (100% general) | +| `.claude/knowledge/governance-module.md` | `docs/knowledge/modules/governance.md` | None (100% general) | +| `.claude/knowledge/social-economy.md` | `docs/knowledge/modules/social-economy.md` | None (100% general) | +| `.claude/knowledge/togetheros-kb.md` | `docs/knowledge/project-overview.md` | ⚠️ Remove Claude-specific sections | +| `.claude/knowledge/ci-cd-discipline.md` | Split into two versions | See below | + +### Phase 2: Split CI/CD Discipline + +**Current:** `.claude/knowledge/ci-cd-discipline.md` has mixed content + +**Split into:** + +1. **`docs/knowledge/guides/ci-cd-for-contributors.md`** (main branch) + - General git workflow + - Commit message format + - PR requirements + - Proof lines concept + - Branch protection + - ❌ Remove: Claude branch naming, YOLO mode, automation specifics + +2. **`.claude/knowledge/ci-cd-discipline.md`** (claude-yolo branch) + - Keep all automation details + - Claude session branches + - YOLO workflow + - Automation retry logic + +### Phase 3: Create Human-Friendly Guides + +**New files for main branch:** + +1. **`docs/contributors/GETTING_STARTED.md`** + - Clone repo + - Install dependencies + - Run locally + - Make first change + - Submit PR + - No automation mentioned + +2. **`docs/contributors/WORKFLOW.md`** + - Branch from main + - Naming conventions (feature/*, fix/*, docs/*) + - Commit message format + - Run validation locally + - Open PR with proof lines + - Code review process + +3. **`docs/contributors/CODE_REVIEW.md`** + - What reviewers look for + - How to respond to feedback + - Approval process + - Merge requirements + +### Phase 4: Main Branch Cleanup + +**Remove from main:** + +```bash +# Directories to remove +.claude/ # Entire directory +codex/ # Or make read-only docs + +# Workflows to remove +.github/workflows/codex-gateway.yml +.github/workflows/codex-preflight.yml +.github/workflows/codex-autolabel.yml +.github/workflows/auto-progress-update.yml # (if Claude-specific) +.github/workflows/sync-github-project.yml # (if Claude-specific) + +# Keep these workflows +.github/workflows/lint.yml +.github/workflows/ci_docs.yml +.github/workflows/smoke.yml +.github/workflows/pr-proof-check.yml +.github/workflows/pr-metadata-preflight.yml +.github/workflows/pr-codex-guard.yml # Or rename to pr-path-guard.yml +.github/workflows/deploy.yml +.github/workflows/admin-delete-runs.yml +``` + +### Phase 5: Update Cross-References + +**Files with internal links to update:** + +1. **README.md** - Point to `docs/knowledge/` instead of `.claude/knowledge/` +2. **docs/OPERATIONS.md** - Update KB references +3. **All module docs** - Update cross-references +4. **CI workflows** - Update validation paths if needed + +--- + +## MERGE STRATEGY: claude-yolo → main + +### What to Merge + +✅ **Include:** +- All application code (apps/web/, packages/) +- Database schema and migrations +- Auth system (Supabase integration) +- User onboarding and dashboard +- Bridge module UI/API +- Design system +- Production config +- Test improvements +- Documentation improvements in `docs/` +- Workflow improvements (lint, docs, smoke) + +❌ **Exclude:** +- `.claude/` directory +- `codex/` directory (or make docs-only) +- Codex automation workflows +- Claude-specific workflows +- Auto-progress-update workflow +- GitHub project sync workflow +- Any scripts that reference Claude/Codex automation + +### Merge Commands + +```bash +# 1. Create merge branch +git checkout -b merge/claude-yolo-to-main main +git fetch origin claude-yolo + +# 2. Selective merge (exclude automation) +git merge origin/claude-yolo --no-commit --no-ff + +# 3. Unstage automation directories +git reset HEAD .claude/ +git reset HEAD codex/ +git reset HEAD .github/workflows/codex-*.yml +git reset HEAD .github/workflows/auto-progress-update.yml +git reset HEAD .github/workflows/sync-github-project.yml + +# 4. Review and commit +git status # Verify what's being merged +git commit -m "merge: integrate claude-yolo features without automation + +Merges application features from claude-yolo: +- Auth system and user onboarding +- Dashboard and profile pages +- Bridge module UI/API +- Design system implementation +- Production configuration +- Workflow optimizations + +Excludes automation systems: +- Claude Code automation (.claude/) +- Codex automation (codex/) +- Automation-specific workflows + +This creates a clean main branch for human contributors while +preserving full automation in claude-yolo branch." + +# 5. Handle conflicts if any +# 6. Test thoroughly +# 7. Merge to main +git checkout main +git merge merge/claude-yolo-to-main +git push origin main +``` + +--- + +## POST-MERGE TASKS + +### 1. Migrate Knowledge Files + +```bash +# On both branches, move files +mkdir -p docs/knowledge/modules +mkdir -p docs/knowledge/guides + +# Move general docs +git mv .claude/knowledge/architecture.md docs/knowledge/ +git mv .claude/knowledge/tech-stack.md docs/knowledge/ +git mv .claude/knowledge/cooperation-paths.md docs/knowledge/ +git mv .claude/knowledge/data-models.md docs/knowledge/ +git mv .claude/knowledge/bridge-module.md docs/knowledge/modules/bridge.md +git mv .claude/knowledge/governance-module.md docs/knowledge/modules/governance.md +git mv .claude/knowledge/social-economy.md docs/knowledge/modules/social-economy.md + +# Create new files +# - docs/knowledge/project-overview.md (clean version of togetheros-kb.md) +# - docs/knowledge/guides/ci-cd-for-contributors.md +# - docs/contributors/GETTING_STARTED.md +# - docs/contributors/WORKFLOW.md +# - docs/contributors/CODE_REVIEW.md +``` + +### 2. Update README.md + +**Main branch version:** + +```markdown +# TogetherOS + +Cooperation-first operating system for communities to self-organize. + +## For Contributors + +- **Getting Started:** [docs/contributors/GETTING_STARTED.md](docs/contributors/GETTING_STARTED.md) +- **Workflow:** [docs/contributors/WORKFLOW.md](docs/contributors/WORKFLOW.md) +- **Project Knowledge:** [docs/knowledge/](docs/knowledge/) + +## Quick Start + +\`\`\`bash +npm install +npm run dev +\`\`\` + +## Documentation + +- [Project Overview](docs/knowledge/project-overview.md) +- [Architecture](docs/knowledge/architecture.md) +- [Tech Stack](docs/knowledge/tech-stack.md) +- [8 Cooperation Paths](docs/knowledge/cooperation-paths.md) + +## Contributing + +See [GETTING_STARTED.md](docs/contributors/GETTING_STARTED.md) for your first contribution. +``` + +### 3. Update `.gitignore` (main branch) + +```gitignore +# Automation systems (not in main branch) +.claude/ +codex/ +``` + +### 4. Create Knowledge Index + +**`docs/knowledge/README.md`:** + +```markdown +# TogetherOS Knowledge Base + +General project documentation for all contributors. + +## Core Concepts + +- [Project Overview](project-overview.md) - Mission, principles, and philosophy +- [Architecture](architecture.md) - Technical design and patterns +- [Tech Stack](tech-stack.md) - Frameworks, tools, and dependencies +- [8 Cooperation Paths](cooperation-paths.md) - Taxonomy and organization + +## Modules + +- [Bridge](modules/bridge.md) - AI assistant for cooperation +- [Governance](modules/governance.md) - Transparent decision-making +- [Social Economy](modules/social-economy.md) - Mutual aid and timebanking + +## Guides + +- [CI/CD for Contributors](guides/ci-cd-for-contributors.md) - Validation and proof lines +- [Getting Started](../contributors/GETTING_STARTED.md) - First contribution +- [Workflow](../contributors/WORKFLOW.md) - Git workflow and conventions +``` + +### 5. Branch Protection Updates + +**Configure on GitHub:** + +**main branch:** +- Require PR reviews: 1 approval +- Require status checks: `ci/lint`, `ci/docs`, `ci/smoke` +- Require proof lines in PR body (via pr-proof-check workflow - make it blocking!) +- No direct pushes to main + +**claude-yolo branch:** +- Same protections as main +- Can have looser requirements for rapid iteration + +--- + +## BENEFITS OF THIS STRUCTURE + +### For Human Contributors (main branch) + +✅ **Clean, inviting repository:** +- No confusing automation directories +- Clear getting started path +- Human-friendly documentation +- Standard git workflow + +✅ **Preserved checks and balances:** +- Lint checks (YAML, Markdown) +- Proof line requirements +- PR metadata validation +- Code review process + +✅ **Easy to understand:** +- `/docs/knowledge/` = project knowledge +- `/docs/contributors/` = how to contribute +- No `.claude/` or `codex/` confusion + +### For AI Automation (claude-yolo branch) + +✅ **Full automation preserved:** +- All Codex features +- Claude YOLO mode +- Notion integration +- Automated progress tracking + +✅ **Access to shared knowledge:** +- Can read `/docs/knowledge/` for general docs +- Can keep `.claude/knowledge/` for automation-specific + +✅ **Dual structure works:** +- Both automation AND human-readable docs +- Symlinks or copies as needed + +### For Project Maintenance + +✅ **Single source of truth:** +- General docs in `/docs/knowledge/` +- No duplication between branches +- Updates propagate to both + +✅ **Clear branch purposes:** +- main = production, human contributors +- claude-yolo = development, full automation + +✅ **Easy to explain:** +- "Want to contribute? Use main branch" +- "Want full automation? Use claude-yolo branch" + +--- + +## IMPLEMENTATION TIMELINE + +### Week 1: Preparation +- Day 1-2: Create `/docs/knowledge/` structure +- Day 3-4: Migrate general docs from `.claude/knowledge/` +- Day 5: Create human contributor guides + +### Week 2: Main Branch Cleanup +- Day 1-2: Selective merge from claude-yolo +- Day 3: Remove automation directories +- Day 4: Update cross-references +- Day 5: Test and verify + +### Week 3: Polish & Documentation +- Day 1-2: Update README, docs index +- Day 3: Create migration guide for contributors +- Day 4-5: Final testing, branch protection setup + +--- + +## DECISION NEEDED + +### Option A: Shared Knowledge via Copy (RECOMMENDED) + +**Both branches have:** `docs/knowledge/` + +**Pros:** +- Independent branches +- No symlink complexity +- Easy to understand +- Main can diverge if needed + +**Cons:** +- Docs can drift between branches +- Updates must be made twice + +**Mitigation:** Periodic sync from claude-yolo → main for docs only + +### Option B: Shared Knowledge via Symlinks + +**Both branches have:** `docs/knowledge/` (real) +**claude-yolo has:** `.claude/knowledge/` → symlinks to `docs/knowledge/` + +**Pros:** +- No duplication +- Single source of truth +- Automatic sync + +**Cons:** +- Symlinks can be confusing +- Git symlink support varies +- More complex to maintain + +**Recommendation:** Use **Option A** for simplicity + +--- + +## NEXT STEPS + +1. **Review this proposal** - Does this structure make sense? + +2. **Decide on merge timing** - Ready to merge claude-yolo → main now? + +3. **Choose knowledge strategy** - Copy or symlinks? + +4. **Create migration branches:** + - `feature/shared-knowledge-structure` - Create `/docs/knowledge/` + - `feature/contributor-guides` - Create `/docs/contributors/` + - `merge/claude-yolo-to-main-clean` - Selective merge without automation + +5. **Execute in order:** + - First: Create shared structure (both branches) + - Second: Migrate docs to shared location (both branches) + - Third: Merge claude-yolo features to main (selective) + - Fourth: Remove automation from main + - Fifth: Update cross-references and indexes + +--- + +## QUESTIONS FOR YOU + +1. **Ready to merge claude-yolo to main?** (You said not yet, but now?) + +2. **Keep codex/ in main as docs-only?** Or remove entirely? + - Option A: Remove entirely + - Option B: Keep `codex/taxonomy/CATEGORY_TREE.json` (just the taxonomy) + - Option C: Keep all codex docs but remove automation + +3. **Knowledge structure preference?** + - Option A: Copy docs to both branches (recommended) + - Option B: Symlinks (complex) + +4. **Should I implement this now?** Or create as a plan document first? + +--- + +**This structure lets main be clean and welcoming for humans while preserving full automation power in claude-yolo. Both branches benefit from shared knowledge, neither duplicates work unnecessarily.** + +Ready to proceed? diff --git a/OPERATIONS.md b/OPERATIONS.md index 77ba9e4b..44f2f7ee 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -1,4 +1,6 @@ # Operations — Start here -For the full operational discipline (proof-lines, tiny verified steps, Codex/GitHub guardrails), **start here**: -- `docs/OPERATIONS_v2.md` +For the full operational discipline (proof-lines, tiny verified steps, CI/CD guardrails), see: +- **Operations Guide:** [docs/OPERATIONS.md](docs/OPERATIONS.md) +- **Quick Start:** [docs/contributors/GETTING_STARTED.md](docs/contributors/GETTING_STARTED.md) +- **Git Workflow:** [docs/contributors/WORKFLOW.md](docs/contributors/WORKFLOW.md) diff --git a/PR104_REVIEW_SUMMARY.md b/PR104_REVIEW_SUMMARY.md new file mode 100644 index 00000000..176a9c43 --- /dev/null +++ b/PR104_REVIEW_SUMMARY.md @@ -0,0 +1,106 @@ +# PR #104 Review & Fix Summary + +**Date:** 2025-10-28 +**PR:** https://github.com/coopeverything/TogetherOS/pull/104 +**Status:** ✅ MERGEABLE (conflicts resolved) + +--- + +## Issues Found & Fixed + +### 1. ✅ Merge Conflicts (5 files) +**Problem:** Feature branch was out of sync with main +**Files Conflicted:** +- `.markdownlint.jsonc` (binary) +- `docs/dev/reward-module-guide.md` +- `docs/modules/INDEX.md` +- `docs/modules/rewards.md` +- `docs/skills/reward-builder-skill.md` + +**Resolution:** +- Accepted main branch versions for all files (they contained newer updates) +- For `docs/modules/INDEX.md`, kept main's improved text: "(one tiny change per PR)" +- Merge commit: `b5474d0` + +### 2. ❓ CI Check Failures +**Observed:** +- `auto-progress-update.yml` workflow failures +- `pr/metadata-preflight` check failure +- Lint and smoke checks mentioned by user + +**Status:** +- Workflow failures appear to be related to the progress update automation +- Not blocking merge now that conflicts are resolved +- CI will re-run on the merge commit + +### 3. ✅ Commit a7e4cb2 Review +**Commit:** `feat(bridge): implement streaming API and NDJSON logging` + +**Findings:** +- ✅ TypeScript path alias `@/lib/*` correctly configured in `apps/web/tsconfig.json:20` +- ✅ All imports from `@/lib/bridge/*` are valid +- ✅ Code structure is clean and follows project patterns +- ✅ No issues detected + +**What a7e4cb2 Added:** +- Bridge streaming API endpoint +- NDJSON privacy-first logging +- Rate limiting (30 req/hour) +- UI component with streaming support +- OpenAI GPT-3.5-turbo integration + +--- + +## Current PR Status + +### Commits in PR (after fixes) +1. `b5474d0` - Merge main into feature/bridge-api-logging (NEW) +2. `a5a5148` - docs: add future explorations tracking document +3. `95eb16d` - feat(bridge): add RAG with docs indexer and source citations +4. `3c32d92` - feat(bridge): Add styling and configuration +5. `a7e4cb2` - feat(bridge): implement streaming API and NDJSON logging + +### Mergeable +✅ **YES** - All conflicts resolved + +### Next Steps +1. Wait for CI checks to complete on `b5474d0` +2. Review Codex suggestions if any +3. Merge when green +4. Deploy to Vercel with `OPENAI_API_KEY` env var + +--- + +## What Was Fixed + +| Issue | Status | Details | +|-------|--------|---------| +| Merge conflicts | ✅ Fixed | 5 files resolved, accepted main versions | +| CI failures | ⏳ Pending | Will rerun on merge commit | +| Commit a7e4cb2 | ✅ Reviewed | No issues found, imports valid | +| PR mergeable | ✅ Yes | Confirmed via GitHub API | + +--- + +## Technical Details + +### Path Alias Configuration +```json +// apps/web/tsconfig.json +"paths": { + "@togetheros/ui": ["../../packages/ui/src"], + "@togetheros/ui/*": ["../../packages/ui/src/*"], + "@/lib/*": ["../../lib/*"] // ← Used by Bridge API +} +``` + +### Files Modified in Merge +- `.markdownlint.jsonc` +- `docs/dev/reward-module-guide.md` +- `docs/modules/INDEX.md` +- `docs/modules/rewards.md` +- `docs/skills/reward-builder-skill.md` + +--- + +**Summary:** PR #104 is now ready to merge. All conflicts resolved, no blocking issues found in commit a7e4cb2. diff --git a/README.md b/README.md index 6298fb4f..66aa48e7 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ OS = Operating System (not “open source”). TogetherOS is a cooperation-first stack that helps people self-organize: direct legislation, mutual aid, a fair social economy, and shared knowledge — all woven into practical tools and clear discipline for contributors. -> 📜 **Manifesto:** see the canonical vision in [docs/Manifesto.md](docs/Manifesto.md) -> 🧭 **Categories & keywords (canonical):** [docs/TogetherOS_CATEGORIES_AND_KEYWORDS.md](docs/TogetherOS_CATEGORIES_AND_KEYWORDS.md) +> 📜 **Manifesto:** see the canonical vision in [docs/Manifesto.md](docs/Manifesto.md) +> 🧭 **Categories & keywords (canonical):** [docs/TogetherOS_CATEGORIES_AND_KEYWORDS](docs/TogetherOS_CATEGORIES_AND_KEYWORDS) ## The 8 Cooperation Paths (canonical) @@ -16,20 +16,19 @@ OS = Operating System (not “open source”). TogetherOS is a cooperation-first - **Collaborative Media & Culture** — stories, film, music, writing, and archives that celebrate cooperation and courage. - **Common Planet** — habitat repair, food forests, circular materials, and climate-positive logistics. -> Canonical names match the Manifesto; use these labels in docs, issues, and UI. -> Canonical names/IDs/keywords live in `docs/TogetherOS_CATEGORIES_AND_KEYWORDS.md` and `codex/taxonomy/CATEGORY_TREE.json`. +> Canonical names match the Manifesto; use these labels in docs, issues, and UI. +> Full taxonomy with keywords: [docs/TogetherOS_CATEGORIES_AND_KEYWORDS](docs/TogetherOS_CATEGORIES_AND_KEYWORDS) | Detailed specs: [docs/cooperation-paths.md](docs/cooperation-paths.md) ## Contributing (minimum PR metadata) -- Start in **[GitHub Discussions #88](https://github.com/coopeverything/TogetherOS/discussions/88)** — say which Path/module you want to help with and propose a tiny first change; we’ll open/assign an issue and point you to a starter task. -- All pull requests should include two proof lines in the description (see `AGENTS.md` for details and taxonomy links): +- Start in **[GitHub Discussions #88](https://github.com/coopeverything/TogetherOS/discussions/88)** — say which Path/module you want to help with and propose a tiny first change; we'll open/assign an issue and point you to a starter task. +- All pull requests should include two proof lines in the description (see [AGENTS.md](AGENTS.md) for details and taxonomy links): ## Docs -- **Ops (start here):** `docs/OPERATIONS.md` -- **Status (seed → tree):** `docs/STATUS_v2.md` and tracker `STATUS/What_we_finished_What_is_left_v2.txt` -- **Vision:** `docs/Manifesto.md` -- **Tech roadmap:** `docs/roadmap/TECH_ROADMAP.md` -- **CI Playbook:** `docs/CI/Actions_Playbook.md` -- **Index:** `docs/INDEX.md` +- **Ops (start here):** [docs/OPERATIONS.md](docs/OPERATIONS.md) +- **Status (seed → tree):** [docs/STATUS_v2.md](docs/STATUS_v2.md) and tracker [STATUS/progress-report.md](STATUS/progress-report.md) +- **Vision:** [docs/Manifesto.md](docs/Manifesto.md) +- **CI Playbook:** [docs/CI/Actions_Playbook.md](docs/CI/Actions_Playbook.md) +- **Index:** [docs/index.md](docs/index.md) diff --git a/STATUS/What_we_finished_What_is_left_v2.txt b/STATUS/What_we_finished_What_is_left_v2.txt deleted file mode 100644 index ccebcada..00000000 --- a/STATUS/What_we_finished_What_is_left_v2.txt +++ /dev/null @@ -1,10 +0,0 @@ -# OPERATIONS (v2) - -Start here → README → **OPERATIONS** → STATUS - -## Contributor rules (short) -Every PR description must include: -# Ensure folder and (re)write tracker with safe ASCII quotes -New-Item -ItemType Directory -Force -Path STATUS | Out-Null -@' -Tracker placeholder. Paste your latest "What we finished — What is left (v2)" here. diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 00000000..5dba4ce8 --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,17 @@ +# OpenAI API Key +OPENAI_API_KEY=your_openai_key_here + +# Bridge Configuration +BRIDGE_RATE_LIMIT_PER_HOUR=30 +BRIDGE_IP_SALT=your_random_salt_here +BRIDGE_ENV=production + +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=togetheros +DB_USER=togetheros_app +DB_PASSWORD=your_secure_password_here + +# Next.js +NEXTAUTH_URL=https://your-domain.com diff --git a/apps/web/.env.production b/apps/web/.env.production new file mode 100644 index 00000000..d545bc37 --- /dev/null +++ b/apps/web/.env.production @@ -0,0 +1,12 @@ +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=togetheros +DB_USER=togetheros_app +DB_PASSWORD=your_actual_db_password_here + +# JWT Secret (for session tokens) +JWT_SECRET=c363bd7e9fd86c7a29b994293f8d2b3f939e7e40705f3186c00a6bae988d0f03 + +# Node Environment +NODE_ENV=production diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 00000000..e985853e --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/apps/web/app/api/auth/login/route.ts b/apps/web/app/api/auth/login/route.ts new file mode 100644 index 00000000..461afe0b --- /dev/null +++ b/apps/web/app/api/auth/login/route.ts @@ -0,0 +1,65 @@ +/** + * User login API + * POST /api/auth/login + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { verifyPassword, logActivity } from '@/lib/db/users'; +import { createSession } from '@/lib/auth/session'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { email, password } = body; + + if (!email || !password) { + return NextResponse.json( + { error: 'Email and password required' }, + { status: 400 } + ); + } + + // Verify credentials + const user = await verifyPassword(email, password); + if (!user) { + return NextResponse.json( + { error: 'Invalid email or password' }, + { status: 401 } + ); + } + + // Create session + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + const userAgent = request.headers.get('user-agent') || ''; + const token = await createSession(user.id, user.email, ip, userAgent); + + // Log activity + await logActivity(user.id, 'login', { ip, userAgent }); + + // Set cookie + const response = NextResponse.json({ + success: true, + user: { + id: user.id, + email: user.email, + name: user.name, + }, + }); + + response.cookies.set('session', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/', + }); + + return response; + } catch (error) { + console.error('Login error:', error); + return NextResponse.json( + { error: 'Failed to login' }, + { status: 500 } + ); + } +} diff --git a/apps/web/app/api/auth/logout/route.ts b/apps/web/app/api/auth/logout/route.ts new file mode 100644 index 00000000..eb3a803c --- /dev/null +++ b/apps/web/app/api/auth/logout/route.ts @@ -0,0 +1,28 @@ +/** + * User logout API + * POST /api/auth/logout + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { deleteSession } from '@/lib/auth/session'; + +export async function POST(request: NextRequest) { + try { + const token = request.cookies.get('session')?.value; + + if (token) { + await deleteSession(token); + } + + const response = NextResponse.json({ success: true }); + response.cookies.delete('session'); + + return response; + } catch (error) { + console.error('Logout error:', error); + return NextResponse.json( + { error: 'Failed to logout' }, + { status: 500 } + ); + } +} diff --git a/apps/web/app/api/auth/signup/route.ts b/apps/web/app/api/auth/signup/route.ts new file mode 100644 index 00000000..66540332 --- /dev/null +++ b/apps/web/app/api/auth/signup/route.ts @@ -0,0 +1,69 @@ +/** + * User signup API + * POST /api/auth/signup + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { createUser, findUserByEmail, logActivity } from '@/lib/db/users'; +import { createSession } from '@/lib/auth/session'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { email, password } = body; + + // Validate email + if (!email || !email.includes('@')) { + return NextResponse.json( + { error: 'Valid email required' }, + { status: 400 } + ); + } + + // Check if user already exists + const existing = await findUserByEmail(email); + if (existing) { + return NextResponse.json( + { error: 'Email already registered' }, + { status: 409 } + ); + } + + // Create user + const user = await createUser(email, password); + + // Create session and log in automatically + const ip = request.headers.get('x-forwarded-for') || request.ip || 'unknown'; + const userAgent = request.headers.get('user-agent') || 'unknown'; + const token = await createSession(user.id, user.email, ip, userAgent); + + // Log activity + await logActivity(user.id, 'signup', { method: 'email', ip, userAgent }); + + const response = NextResponse.json({ + success: true, + user: { + id: user.id, + email: user.email, + created_at: user.created_at, + }, + }); + + // Set session cookie + response.cookies.set('session', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/', + }); + + return response; + } catch (error) { + console.error('Signup error:', error); + return NextResponse.json( + { error: 'Failed to create account' }, + { status: 500 } + ); + } +} diff --git a/apps/web/app/api/bridge/ask/route.ts b/apps/web/app/api/bridge/ask/route.ts new file mode 100644 index 00000000..06e60ca2 --- /dev/null +++ b/apps/web/app/api/bridge/ask/route.ts @@ -0,0 +1,271 @@ +/** + * Bridge API Endpoint: POST /api/bridge/ask + * + * Streaming Q&A endpoint for Bridge landing pilot + * Features: Rate limiting, NDJSON logging, privacy-first, RAG with docs + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { join } from 'path'; +import { checkRateLimit } from '@/lib/bridge/rate-limiter'; +import { logBridgeAction, getClientIp, hashIp } from '@/lib/bridge/logger'; +import { + buildIndex, + getRelevantExcerpts, + getSources, + type DocEntry, +} from '@/lib/bridge/docs-indexer'; + +const RATE_LIMIT_MAX = parseInt(process.env.BRIDGE_RATE_LIMIT_PER_HOUR || '30', 10); +const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour in ms + +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const BRIDGE_SYSTEM_PROMPT = `You are Bridge, the assistant of TogetherOS. Speak plainly, avoid jargon, and emphasize cooperation, empathy, and human decision-making. Answer only what was asked. Prefer concrete examples over abstractions and be concise.`; + +// Cache the document index in memory +let docsIndex: DocEntry[] | null = null; + +function getDocsIndex(): DocEntry[] { + if (!docsIndex) { + try { + // Build index from /docs directory + const docsPath = join(process.cwd(), '..', '..', 'docs'); + docsIndex = buildIndex(docsPath); + console.log(`[Bridge] Indexed ${docsIndex.length} documents`); + } catch (error) { + console.error('[Bridge] Error building docs index:', error); + docsIndex = []; + } + } + return docsIndex; +} + +export async function POST(request: NextRequest) { + const startTime = Date.now(); + + // Get client IP + const clientIp = getClientIp(request); + const ipHash = hashIp(clientIp); + + try { + // Parse request body + const body = await request.json().catch(() => ({})); + const question = body.question?.trim(); + + // Validate input - 204 for empty + if (!question || question.length === 0) { + logBridgeAction({ + action: 'error', + ip_hash: ipHash, + status: 204, + latency_ms: Date.now() - startTime, + }); + return new NextResponse(null, { status: 204 }); + } + + // Check API key - 401 for missing/invalid + if (!OPENAI_API_KEY) { + logBridgeAction({ + action: 'error', + ip_hash: ipHash, + q_len: question.length, + status: 401, + error: 'API key not configured', + latency_ms: Date.now() - startTime, + }); + return NextResponse.json( + { error: 'Service not configured' }, + { status: 401 } + ); + } + + // Rate limiting - 429 for exceeded + const rateLimit = checkRateLimit(ipHash, { + maxRequests: RATE_LIMIT_MAX, + windowMs: RATE_LIMIT_WINDOW, + }); + + if (!rateLimit.allowed) { + logBridgeAction({ + action: 'rate_limit', + ip_hash: ipHash, + q_len: question.length, + status: 429, + latency_ms: Date.now() - startTime, + }); + + const resetInSeconds = Math.ceil((rateLimit.resetAt - Date.now()) / 1000); + return NextResponse.json( + { + error: 'Rate limit exceeded', + message: `Please wait ${resetInSeconds} seconds before trying again`, + resetAt: rateLimit.resetAt, + }, + { + status: 429, + headers: { + 'X-RateLimit-Limit': RATE_LIMIT_MAX.toString(), + 'X-RateLimit-Remaining': '0', + 'X-RateLimit-Reset': rateLimit.resetAt.toString(), + }, + } + ); + } + + // Get relevant documentation context (RAG) + const index = getDocsIndex(); + const context = getRelevantExcerpts(index, question, 1500); + const sources = getSources(index, question, 3); + + // Build enhanced system prompt with context + const enhancedSystemPrompt = context + ? `${BRIDGE_SYSTEM_PROMPT} + +Use the following documentation to inform your answer: + +${context} + +Cite sources when relevant using the format [Source: title].` + : BRIDGE_SYSTEM_PROMPT; + + // Call OpenAI API with streaming + const openaiResponse = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', + messages: [ + { role: 'system', content: enhancedSystemPrompt }, + { role: 'user', content: question }, + ], + stream: true, + max_tokens: 500, + temperature: 0.7, + }), + }); + + if (!openaiResponse.ok) { + const errorData = await openaiResponse.json().catch(() => ({})); + const errorMessage = errorData.error?.message || `OpenAI API error: ${openaiResponse.status}`; + + // Handle specific OpenAI errors + if (openaiResponse.status === 401) { + logBridgeAction({ + action: 'error', + ip_hash: ipHash, + q_len: question.length, + status: 401, + error: 'Invalid OpenAI API key', + latency_ms: Date.now() - startTime, + }); + return NextResponse.json( + { error: 'Service authentication failed' }, + { status: 401 } + ); + } + + if (openaiResponse.status === 429) { + logBridgeAction({ + action: 'error', + ip_hash: ipHash, + q_len: question.length, + status: 429, + error: 'OpenAI rate limit exceeded', + latency_ms: Date.now() - startTime, + }); + return NextResponse.json( + { error: 'Service temporarily unavailable. Please try again later.' }, + { status: 503 } + ); + } + + throw new Error(errorMessage); + } + + // Log successful request + logBridgeAction({ + action: 'ask', + ip_hash: ipHash, + q_len: question.length, + latency_ms: Date.now() - startTime, + }); + + // Stream response back to client + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + async start(controller) { + const reader = openaiResponse.body?.getReader(); + if (!reader) { + controller.close(); + return; + } + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // Parse SSE format from OpenAI + const text = new TextDecoder().decode(value); + const lines = text.split('\n').filter((line) => line.trim() !== ''); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') continue; + + try { + const json = JSON.parse(data); + const content = json.choices?.[0]?.delta?.content; + if (content) { + controller.enqueue(encoder.encode(content)); + } + } catch (e) { + // Skip malformed JSON + } + } + } + } + + // Append sources at the end + if (sources.length > 0) { + const sourcesText = '\n\n---\n\n**Sources:**\n' + + sources.map(s => `- [${s.title}](https://github.com/coopeverything/TogetherOS/blob/main/docs/${s.path})`).join('\n'); + controller.enqueue(encoder.encode(sourcesText)); + } + } catch (error) { + console.error('Stream error:', error); + } finally { + controller.close(); + } + }, + }); + + return new NextResponse(stream, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'X-RateLimit-Limit': RATE_LIMIT_MAX.toString(), + 'X-RateLimit-Remaining': rateLimit.remaining.toString(), + 'X-RateLimit-Reset': rateLimit.resetAt.toString(), + }, + }); + } catch (error) { + // 500 for unexpected errors + console.error('Bridge API error:', error); + logBridgeAction({ + action: 'error', + ip_hash: ipHash, + status: 500, + error: error instanceof Error ? error.message : 'Unknown error', + latency_ms: Date.now() - startTime, + }); + + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/apps/web/app/api/onboarding/complete/route.ts b/apps/web/app/api/onboarding/complete/route.ts new file mode 100644 index 00000000..750ec637 --- /dev/null +++ b/apps/web/app/api/onboarding/complete/route.ts @@ -0,0 +1,28 @@ +/** + * Complete Onboarding API + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { getCurrentUser } from '@/lib/auth/middleware'; +import { query } from '@/lib/db'; + +export async function POST(request: NextRequest) { + try { + const user = await getCurrentUser(request); + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Mark onboarding as complete + await query( + 'UPDATE users SET onboarding_completed_at = NOW(), onboarding_step = $1 WHERE id = $2', + ['completed', user.id] + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Complete onboarding error:', error); + return NextResponse.json({ error: 'Failed to complete onboarding' }, { status: 500 }); + } +} diff --git a/apps/web/app/api/profile/route.ts b/apps/web/app/api/profile/route.ts new file mode 100644 index 00000000..b56c224c --- /dev/null +++ b/apps/web/app/api/profile/route.ts @@ -0,0 +1,84 @@ +/** + * User Profile API + * GET - Get current user profile + * PATCH - Update user profile + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { getCurrentUser } from '@/lib/auth/middleware'; +import { updateUser } from '@/lib/db/users'; + +export async function GET(request: NextRequest) { + try { + const user = await getCurrentUser(request); + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return NextResponse.json({ user }); + } catch (error) { + console.error('Get profile error:', error); + return NextResponse.json({ error: 'Failed to get profile' }, { status: 500 }); + } +} + +export async function PATCH(request: NextRequest) { + try { + const user = await getCurrentUser(request); + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + const { + name, + username, + bio, + avatar_url, + city, + state, + country, + paths, + skills, + can_offer, + seeking_help, + } = body; + + // Validate username format if provided + if (username && !/^[a-zA-Z0-9_-]{3,50}$/.test(username)) { + return NextResponse.json( + { error: 'Username must be 3-50 characters and contain only letters, numbers, underscores, and hyphens' }, + { status: 400 } + ); + } + + const updatedUser = await updateUser(user.id, { + name, + username, + bio, + avatar_url, + city, + state, + country, + paths, + skills, + can_offer, + seeking_help, + }); + + return NextResponse.json({ user: updatedUser }); + } catch (error: any) { + console.error('Update profile error:', error); + + // Handle unique constraint violations + if (error.code === '23505') { + if (error.constraint === 'users_username_key') { + return NextResponse.json({ error: 'Username already taken' }, { status: 409 }); + } + } + + return NextResponse.json({ error: 'Failed to update profile' }, { status: 500 }); + } +} diff --git a/apps/web/app/bridge/page.tsx b/apps/web/app/bridge/page.tsx new file mode 100644 index 00000000..fb0ab0f2 --- /dev/null +++ b/apps/web/app/bridge/page.tsx @@ -0,0 +1,24 @@ +/** + * Bridge Landing Page + * + * Minimal public page where visitors can ask "What is TogetherOS?" + * and get a calm, mission-first answer. + * + * Part of Bridge Landing Pilot (internal MVP) + * @see docs/modules/bridge/landing-pilot.md + */ + +import { BridgeChat } from '@togetheros/ui/bridge'; + +export const metadata = { + title: 'Bridge - TogetherOS', + description: 'Ask Bridge what TogetherOS is.', +}; + +export default function BridgePage() { + return ( +
+ +
+ ); +} diff --git a/apps/web/app/dashboard/DashboardClient.tsx b/apps/web/app/dashboard/DashboardClient.tsx new file mode 100644 index 00000000..4ac68921 --- /dev/null +++ b/apps/web/app/dashboard/DashboardClient.tsx @@ -0,0 +1,164 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import styles from './dashboard.module.css'; + +interface User { + id: string; + email: string; + name?: string; + bio?: string; + avatar_url?: string; + paths?: string[]; + skills?: string[]; + onboarding_step?: string; +} + +const COOPERATION_PATHS = [ + { id: 'education', name: 'Collaborative Education', emoji: '📚', color: '#3B82F6' }, + { id: 'economy', name: 'Social Economy', emoji: '💰', color: '#10B981' }, + { id: 'wellbeing', name: 'Common Wellbeing', emoji: '🫶', color: '#EC4899' }, + { id: 'technology', name: 'Cooperative Technology', emoji: '💻', color: '#8B5CF6' }, + { id: 'governance', name: 'Collective Governance', emoji: '🏛️', color: '#F59E0B' }, + { id: 'community', name: 'Community Connection', emoji: '🤝', color: '#EF4444' }, + { id: 'media', name: 'Collaborative Media', emoji: '🎨', color: '#6366F1' }, + { id: 'planet', name: 'Common Planet', emoji: '🌍', color: '#059669' }, +]; + +export default function DashboardClient({ user }: { user: User }) { + const router = useRouter(); + + const handleLogout = async () => { + await fetch('/api/auth/logout', { method: 'POST' }); + router.push('/login'); + }; + + const userPaths = user.paths || []; + + return ( +
+ {/* Header */} +
+
+

+ Welcome back{user.name ? `, ${user.name}` : ''}! +

+
+ + +
+
+
+ +
+ {/* Stats Cards */} +
+
+
👥
+
+
1,247
+
Active Members
+
+
+ +
+
📋
+
+
23
+
Open Proposals
+
+
+ +
+
🤲
+
+
89
+
Mutual Aid Requests
+
+
+ +
+
+
+
{userPaths.length}/8
+
Your Paths
+
+
+
+ + {/* Your Journey */} + {userPaths.length > 0 && ( +
+

Your Cooperation Paths

+
+ {COOPERATION_PATHS.filter((p) => userPaths.includes(p.id)).map((path) => ( +
+
{path.emoji}
+
{path.name}
+
+ ))} +
+
+ )} + + {/* Cooperation Paths Explorer */} +
+

Explore Cooperation Paths

+

+ Choose the paths that resonate with you. You can always add or remove paths later. +

+
+ {COOPERATION_PATHS.map((path) => { + const isActive = userPaths.includes(path.id); + return ( +
+
{path.emoji}
+
{path.name}
+ {isActive &&
Active
} +
+ ); + })} +
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + + + + + + +
+
+
+
+ ); +} diff --git a/apps/web/app/dashboard/dashboard.module.css b/apps/web/app/dashboard/dashboard.module.css new file mode 100644 index 00000000..bcb71218 --- /dev/null +++ b/apps/web/app/dashboard/dashboard.module.css @@ -0,0 +1,276 @@ +/* Dashboard - TogetherOS Design System */ + +.container { + min-height: 100vh; + background: var(--bg-0, #FAFAF9); +} + +.header { + background: var(--bg-1, #FFFFFF); + border-bottom: 1px solid var(--border, #E5E7EB); + padding: 1.5rem 0; + position: sticky; + top: 0; + z-index: 10; +} + +.headerContent { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.welcomeTitle { + font-size: 1.5rem; + font-weight: 700; + color: var(--ink-900, #0F172A); + margin: 0; +} + +.headerActions { + display: flex; + gap: 0.75rem; +} + +.headerButton, .headerButtonSecondary { + padding: 0.5rem 1rem; + font-size: 0.9375rem; + font-weight: 600; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.15s ease-out; + border: none; + font-family: inherit; +} + +.headerButton { + background: var(--brand-600, #059669); + color: white; +} + +.headerButton:hover { + background: #047857; +} + +.headerButtonSecondary { + background: var(--bg-2, #F5F5F4); + color: var(--ink-700, #334155); +} + +.headerButtonSecondary:hover { + background: var(--border, #E5E7EB); +} + +.main { + max-width: 1200px; + margin: 0 auto; + padding: 3rem 2rem; +} + +/* Stats Section */ +.statsSection { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 3rem; +} + +.statCard { + background: var(--bg-1, #FFFFFF); + border: 1px solid var(--border, #E5E7EB); + border-radius: 1rem; + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.statIcon { + font-size: 2.5rem; +} + +.statContent { + flex: 1; +} + +.statValue { + font-size: 2rem; + font-weight: 700; + color: var(--ink-900, #0F172A); + line-height: 1; +} + +.statLabel { + font-size: 0.875rem; + color: var(--ink-400, #94A3B8); + margin-top: 0.25rem; +} + +/* Journey Section */ +.journeySection { + margin-bottom: 3rem; +} + +.sectionTitle { + font-size: 1.75rem; + font-weight: 700; + color: var(--ink-900, #0F172A); + margin-bottom: 1rem; +} + +.sectionDesc { + font-size: 1.0625rem; + color: var(--ink-700, #334155); + line-height: 1.6; + margin-bottom: 1.5rem; +} + +.pathsList { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.activePathCard { + background: var(--brand-100, #D1FAE5); + border: 1px solid var(--brand-500, #10B981); + border-radius: 0.75rem; + padding: 0.75rem 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.pathEmoji { + font-size: 1.25rem; +} + +.pathName { + font-size: 0.9375rem; + font-weight: 600; + color: var(--brand-600, #059669); +} + +/* Paths Explorer */ +.pathsSection { + margin-bottom: 3rem; +} + +.pathsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; +} + +.pathCard { + background: var(--bg-1, #FFFFFF); + border: 2px solid var(--border, #E5E7EB); + border-radius: 1rem; + padding: 2rem 1.5rem; + text-align: center; + cursor: pointer; + transition: all 0.15s ease-out; + position: relative; +} + +.pathCard:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.pathCardActive { + border-width: 2px; + background: var(--brand-100, #D1FAE5); +} + +.pathCardEmoji { + font-size: 3rem; + margin-bottom: 0.75rem; +} + +.pathCardName { + font-size: 1.0625rem; + font-weight: 600; + color: var(--ink-900, #0F172A); +} + +.pathCardBadge { + position: absolute; + top: 0.75rem; + right: 0.75rem; + background: var(--brand-600, #059669); + color: white; + font-size: 0.75rem; + font-weight: 600; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; +} + +/* Actions Section */ +.actionsSection { + margin-bottom: 3rem; +} + +.actionsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; +} + +.actionCard { + background: var(--bg-1, #FFFFFF); + border: 1px solid var(--border, #E5E7EB); + border-radius: 1rem; + padding: 2rem 1.5rem; + text-align: left; + cursor: pointer; + transition: all 0.15s ease-out; + font-family: inherit; +} + +.actionCard:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: var(--brand-500, #10B981); +} + +.actionIcon { + font-size: 2.5rem; + margin-bottom: 0.75rem; +} + +.actionTitle { + font-size: 1.125rem; + font-weight: 700; + color: var(--ink-900, #0F172A); + margin-bottom: 0.5rem; +} + +.actionDesc { + font-size: 0.9375rem; + color: var(--ink-700, #334155); + line-height: 1.6; +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .headerContent { + flex-direction: column; + gap: 1rem; + } + + .main { + padding: 2rem 1rem; + } + + .statsSection { + grid-template-columns: 1fr; + } + + .pathsGrid, .actionsGrid { + grid-template-columns: 1fr; + } +} diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx new file mode 100644 index 00000000..c3d5ae49 --- /dev/null +++ b/apps/web/app/dashboard/page.tsx @@ -0,0 +1,39 @@ +/** + * User Dashboard + * Protected route - requires authentication + */ + +import { redirect } from 'next/navigation'; +import { cookies } from 'next/headers'; +import { verifySession } from '@/lib/auth/session'; +import { findUserById } from '@/lib/db/users'; +import DashboardClient from './DashboardClient'; + +export default async function DashboardPage() { + // Get session from cookies + const cookieStore = await cookies(); + const sessionToken = cookieStore.get('session')?.value; + + if (!sessionToken) { + redirect('/login'); + } + + // Verify session + const session = verifySession(sessionToken); + if (!session) { + redirect('/login'); + } + + // Get user + const user = await findUserById(session.userId); + if (!user) { + redirect('/login'); + } + + // If user hasn't completed onboarding, redirect + if (!user.onboarding_completed_at) { + redirect('/onboarding'); + } + + return ; +} diff --git a/apps/web/app/design/page.tsx b/apps/web/app/design/page.tsx new file mode 100644 index 00000000..db4d2a88 --- /dev/null +++ b/apps/web/app/design/page.tsx @@ -0,0 +1,520 @@ +'use client'; + +import { useState } from 'react'; + +export default function DesignShowcase() { + const [darkMode, setDarkMode] = useState(false); + const [dashboardMode, setDashboardMode] = useState<'calm' | 'compact'>('calm'); + + return ( +
+ + +
+ {/* Header */} +
+
+

+ TogetherOS Design System +

+ +
+
+ +
+ + {/* Philosophy */} +
+

+ Warm Minimalism +

+

+ Clean, joyful, and restful. Lots of white space, soft neutrals, one lively accent, + and a gentle warm companion. Text stays dark and readable; accents do the emotional work. +

+
+ + {/* Color Palette */} +
+

+ Color Palette +

+ +
+ {/* Backgrounds */} + + + {/* Text */} + + + {/* Brand */} + + + {/* Joy */} + + + {/* Semantic */} + +
+
+ + {/* Typography */} +
+

+ Typography +

+
+

+ Heading 1 (36px, Bold) +

+

+ Heading 2 (28px, Bold) +

+

+ Body text (18px, Regular). Maximum line length of 68-72 characters keeps reading + comfortable. Line height of 1.6+ gives the text room to breathe. +

+

+ Muted text (16px, Regular) for secondary information and captions. +

+
+
+ + {/* Buttons */} +
+

+ Buttons +

+
+ + + + +
+
+ + {/* Dashboard Mockup */} +
+
+

+ Dashboard Example +

+
+ + +
+
+ +
+ + + + +
+
+ + {/* Cards */} +
+

+ Cards & Panels +

+
+
+

+ Standard Card +

+

+ Roomy padding (2rem), clean typography, and gentle borders. One action per card. +

+ +
+ +
+

+ Highlighted Card +

+

+ Soft accent background draws attention to "what matters now" without overwhelming. +

+ +
+ +
+

+ Subtle Panel +

+

+ Background panels for secondary content. Lower contrast keeps visual hierarchy clear. +

+ +
+
+
+ + {/* Usage Rules */} +
+

+ Design Principles +

+
+
    +
  • One accent per screen — Choose either brand or joy as the hero, not both
  • +
  • Big, breathable panels — Default padding ≥ 2rem, line height 1.6+
  • +
  • Space first, borders second — Separate sections with whitespace
  • +
  • Typography cap — Max 68-72 characters per line for readability
  • +
  • Micro transitions — 150-200ms ease-out, no parallax or looping animations
  • +
  • Accessibility — Body text ≥ WCAG AA (aim 7:1 contrast)
  • +
+
+
+ +
+
+
+ ); +} + +function ColorGroup({ title, colors }: { title: string; colors: Array<{ name: string; value: string; bg?: string }> }) { + return ( +
+

+ {title} +

+
+ {colors.map((color) => ( +
+
+
+
{color.name}
+
{color.value}
+
+
+ ))} +
+
+ ); +} + +function DashboardTile({ + title, + value, + change, + trend, + accent, + mode +}: { + title: string; + value: string; + change: string; + trend: 'up' | 'down' | 'neutral'; + accent: 'brand' | 'joy' | 'success' | 'warn'; + mode: 'calm' | 'compact'; +}) { + const accentColors = { + brand: 'var(--brand-500)', + joy: 'var(--joy-500)', + success: 'var(--success)', + warn: 'var(--warn)' + }; + + const trendIcons = { + up: '↗', + down: '↘', + neutral: '→' + }; + + return ( +
+
+

+ {title} +

+ {trendIcons[trend]} +
+ +
+ {value} +
+ +
+ {change} +
+ + {mode === 'calm' && ( + + )} +
+ ); +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css new file mode 100644 index 00000000..d74ec2df --- /dev/null +++ b/apps/web/app/globals.css @@ -0,0 +1,115 @@ +/** + * TogetherOS Global Styles + * Design System: Warm Minimalism + */ + +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* CSS Variables - Light Mode (Default) */ +:root { + /* Backgrounds & Surfaces */ + --bg-0: #FAFAF9; + --bg-1: #FFFFFF; + --bg-2: #F5F5F4; + + /* Text & Neutrals */ + --ink-900: #0F172A; + --ink-700: #334155; + --ink-400: #94A3B8; + --border: #E5E7EB; + + /* Brand - Cooperative Green */ + --brand-600: #059669; + --brand-500: #10B981; + --brand-100: #D1FAE5; + + /* Joy - Apricot */ + --joy-600: #F59E0B; + --joy-500: #FDBA74; + --joy-100: #FFF7ED; + + /* Semantic Colors */ + --success: #16A34A; + --success-bg: #DCFCE7; + --info: #0EA5E9; + --info-bg: #E0F2FE; + --warn: #D97706; + --warn-bg: #FEF3C7; + --danger: #DC2626; + --danger-bg: #FEE2E2; +} + +/* Dark Mode */ +.dark { + --bg-0: #0B0F14; + --bg-1: #0F141A; + --bg-2: #121922; + --ink-900: #E5E7EB; + --ink-700: #CBD5E1; + --ink-400: #94A3B8; + --border: #1F2937; + --brand-500: #22C55E; + --joy-500: #FBBF24; +} + +/* Base Styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-0); + color: var(--ink-900); + line-height: 1.6; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + color: var(--ink-900); +} + +h1 { + font-size: 2.25rem; +} + +h2 { + font-size: 1.75rem; +} + +h3 { + font-size: 1.5rem; +} + +a { + color: var(--brand-600); + text-decoration: none; +} + +a:hover { + color: var(--brand-500); + text-decoration: underline; +} + +/* Reduced Motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 00000000..b6112bff --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,24 @@ +/** + * Root Layout for TogetherOS + * + * This is the top-level layout that wraps all pages in the application. + */ + +import './globals.css'; + +export const metadata = { + title: 'TogetherOS', + description: 'A cooperative operating system for collective action', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx new file mode 100644 index 00000000..df28e3e7 --- /dev/null +++ b/apps/web/app/login/page.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { useState, FormEvent } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import styles from '../signup/signup.module.css'; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [state, setState] = useState<'idle' | 'loading' | 'error'>('idle'); + const [errorMessage, setErrorMessage] = useState(''); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + if (!email.trim() || !password) { + setErrorMessage('Email and password are required'); + return; + } + + setState('loading'); + setErrorMessage(''); + + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email.trim(), password }), + }); + + const data = await response.json(); + + if (!response.ok) { + setState('error'); + setErrorMessage(data.error || 'Failed to login'); + return; + } + + // Redirect to dashboard + router.push('/dashboard'); + } catch (error) { + console.error('Login error:', error); + setState('error'); + setErrorMessage('Failed to connect. Please try again.'); + } + }; + + return ( +
+
+

Welcome back

+ +

+ Sign in to continue your cooperation journey. +

+ +
+
+ + setEmail(e.target.value)} + placeholder="you@example.com" + className={styles.input} + disabled={state === 'loading'} + required + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="Your password" + className={styles.input} + disabled={state === 'loading'} + required + /> +
+ + {state === 'error' && ( +
{errorMessage}
+ )} + + + +
+ or sign in with +
+ +
+ + +
+
+ +

+ Don't have an account? Sign up +

+
+
+ ); +} diff --git a/apps/web/app/onboarding/OnboardingClient.tsx b/apps/web/app/onboarding/OnboardingClient.tsx new file mode 100644 index 00000000..950d6f32 --- /dev/null +++ b/apps/web/app/onboarding/OnboardingClient.tsx @@ -0,0 +1,275 @@ +'use client'; + +import { useState, FormEvent } from 'react'; +import { useRouter } from 'next/navigation'; +import styles from './onboarding.module.css'; + +interface User { + id: string; + email: string; + name?: string; + paths?: string[]; + skills?: string[]; +} + +const COOPERATION_PATHS = [ + { id: 'education', name: 'Collaborative Education', emoji: '📚', desc: 'Learning together, teaching each other' }, + { id: 'economy', name: 'Social Economy', emoji: '💰', desc: 'Fair trade, worker ownership, mutual aid' }, + { id: 'wellbeing', name: 'Common Wellbeing', emoji: '🫶', desc: 'Mental health, physical health, community care' }, + { id: 'technology', name: 'Cooperative Technology', emoji: '💻', desc: 'Open source, ethical tech, digital commons' }, + { id: 'governance', name: 'Collective Governance', emoji: '🏛️', desc: 'Democratic decision-making, consensus building' }, + { id: 'community', name: 'Community Connection', emoji: '🤝', desc: 'Building relationships, organizing locally' }, + { id: 'media', name: 'Collaborative Media', emoji: '🎨', desc: 'Independent media, creative commons, storytelling' }, + { id: 'planet', name: 'Common Planet', emoji: '🌍', desc: 'Sustainability, climate action, ecological justice' }, +]; + +export default function OnboardingClient({ user }: { user: User }) { + const router = useRouter(); + const [step, setStep] = useState(1); + const [state, setState] = useState<'idle' | 'saving' | 'error'>('idle'); + const [errorMessage, setErrorMessage] = useState(''); + + const [formData, setFormData] = useState({ + name: user.name || '', + paths: user.paths || [], + skills: '', + }); + + const totalSteps = 4; + + const handleNext = () => { + if (step < totalSteps) { + setStep(step + 1); + } + }; + + const handleBack = () => { + if (step > 1) { + setStep(step - 1); + } + }; + + const togglePath = (pathId: string) => { + const newPaths = formData.paths.includes(pathId) + ? formData.paths.filter((p) => p !== pathId) + : [...formData.paths, pathId]; + setFormData({ ...formData, paths: newPaths }); + }; + + const handleComplete = async () => { + setState('saving'); + setErrorMessage(''); + + try { + // Update profile + const profileResponse = await fetch('/api/profile', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: formData.name, + paths: formData.paths, + skills: formData.skills + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }), + }); + + if (!profileResponse.ok) { + const data = await profileResponse.json(); + throw new Error(data.error || 'Failed to update profile'); + } + + // Mark onboarding as complete + const onboardingResponse = await fetch('/api/onboarding/complete', { + method: 'POST', + }); + + if (!onboardingResponse.ok) { + throw new Error('Failed to complete onboarding'); + } + + // Redirect to dashboard + router.push('/dashboard'); + } catch (error: any) { + console.error('Complete onboarding error:', error); + setState('error'); + setErrorMessage(error.message || 'Failed to complete. Please try again.'); + } + }; + + const canProceed = () => { + if (step === 2) return formData.name.trim().length > 0; + if (step === 3) return formData.paths.length > 0; + return true; + }; + + return ( +
+
+ {/* Progress Bar */} +
+
+
+ + {/* Step 1: Welcome */} + {step === 1 && ( +
+

Welcome to TogetherOS

+

+ You've just joined a community building a new way to organize—where cooperation + replaces competition, where communities solve their own problems, and where your + skills actually matter. +

+

+ Let's take a few moments to set up your profile so you can connect with the right + people and projects. +

+ +
+ )} + + {/* Step 2: Name */} + {step === 2 && ( +
+

What should we call you?

+

+ This can be your real name, a nickname, or whatever you'd like to be known as in + the community. +

+ +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Your name" + className={styles.input} + autoFocus + /> +
+ +
+ + +
+
+ )} + + {/* Step 3: Paths */} + {step === 3 && ( +
+

Choose Your Paths

+

+ Select the areas you're interested in. You can choose as many as you like, and you + can always change these later. +

+ +
+ {COOPERATION_PATHS.map((path) => { + const isSelected = formData.paths.includes(path.id); + return ( + + ); + })} +
+ +
+ + +
+
+ )} + + {/* Step 4: Skills (Optional) */} + {step === 4 && ( +
+

What skills can you share?

+

+ This is optional, but it helps others know what you can contribute. You can add + more skills anytime from your profile. +

+ +
+ + setFormData({ ...formData, skills: e.target.value })} + placeholder="e.g. Web Development, Graphic Design, Community Organizing" + className={styles.input} + /> +

Separate multiple skills with commas

+
+ + {state === 'error' &&
{errorMessage}
} + +
+ + +
+ + +
+ )} + + {/* Step Indicator */} +
+ Step {step} of {totalSteps} +
+
+
+ ); +} diff --git a/apps/web/app/onboarding/onboarding.module.css b/apps/web/app/onboarding/onboarding.module.css new file mode 100644 index 00000000..2d364444 --- /dev/null +++ b/apps/web/app/onboarding/onboarding.module.css @@ -0,0 +1,274 @@ +/* Onboarding Flow - TogetherOS Design System */ + +.container { + min-height: 100vh; + background: var(--bg-0, #FAFAF9); + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.card { + background: var(--bg-1, #FFFFFF); + border: 1px solid var(--border, #E5E7EB); + border-radius: 1rem; + padding: 3rem; + max-width: 800px; + width: 100%; + position: relative; +} + +.progress { + height: 4px; + background: var(--bg-2, #F5F5F4); + border-radius: 2px; + margin-bottom: 3rem; + overflow: hidden; +} + +.progressBar { + height: 100%; + background: var(--brand-600, #059669); + transition: width 0.3s ease-out; +} + +.step { + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.title { + font-size: 2rem; + font-weight: 700; + color: var(--ink-900, #0F172A); + margin: 0 0 1rem 0; + text-align: center; +} + +.intro { + font-size: 1.0625rem; + color: var(--ink-700, #334155); + line-height: 1.7; + margin-bottom: 2rem; + text-align: center; +} + +.inputGroup { + margin-bottom: 2rem; +} + +.label { + display: block; + font-size: 0.9375rem; + font-weight: 600; + color: var(--ink-700, #334155); + margin-bottom: 0.5rem; +} + +.input { + width: 100%; + padding: 0.875rem; + font-size: 1rem; + font-family: inherit; + color: var(--ink-900, #0F172A); + background: var(--bg-0, #FAFAF9); + border: 1px solid var(--border, #E5E7EB); + border-radius: 0.5rem; + transition: all 0.15s ease-out; +} + +.input:focus { + outline: none; + border-color: var(--brand-500, #10B981); + box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.1); +} + +.hint { + font-size: 0.8125rem; + color: var(--ink-400, #94A3B8); + margin-top: 0.5rem; +} + +.pathsGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.pathCard { + background: var(--bg-0, #FAFAF9); + border: 2px solid var(--border, #E5E7EB); + border-radius: 0.75rem; + padding: 1.5rem 1rem; + cursor: pointer; + transition: all 0.15s ease-out; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + position: relative; + font-family: inherit; +} + +.pathCard:hover { + transform: translateY(-2px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.pathCardActive { + background: var(--brand-100, #D1FAE5); + border-color: var(--brand-500, #10B981); +} + +.pathEmoji { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.pathName { + font-size: 0.875rem; + font-weight: 600; + color: var(--ink-900, #0F172A); + margin-bottom: 0.5rem; +} + +.pathDesc { + font-size: 0.75rem; + color: var(--ink-600, #475569); + line-height: 1.4; +} + +.checkmark { + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 1.5rem; + height: 1.5rem; + background: var(--brand-600, #059669); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + font-weight: 700; +} + +.buttonGroup { + display: flex; + gap: 1rem; + margin-top: 2rem; +} + +.button, .buttonSecondary { + flex: 1; + padding: 0.875rem 1.5rem; + font-size: 1rem; + font-weight: 600; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.15s ease-out; + border: none; + font-family: inherit; +} + +.button { + background: var(--brand-600, #059669); + color: white; +} + +.button:hover:not(:disabled) { + background: #047857; +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.buttonSecondary { + background: var(--bg-2, #F5F5F4); + color: var(--ink-700, #334155); +} + +.buttonSecondary:hover { + background: var(--border, #E5E7EB); +} + +.skipButton { + width: 100%; + padding: 0.75rem; + font-size: 0.9375rem; + font-weight: 500; + color: var(--ink-600, #475569); + background: transparent; + border: none; + cursor: pointer; + transition: color 0.15s ease-out; + margin-top: 1rem; + font-family: inherit; +} + +.skipButton:hover:not(:disabled) { + color: var(--ink-900, #0F172A); +} + +.skipButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.error { + background: #FEE2E2; + color: #991B1B; + padding: 1rem; + border-radius: 0.5rem; + margin-top: 1rem; + font-size: 0.9375rem; +} + +.stepIndicator { + text-align: center; + margin-top: 2rem; + font-size: 0.875rem; + color: var(--ink-400, #94A3B8); +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .card { + padding: 2rem 1.5rem; + } + + .title { + font-size: 1.5rem; + } + + .intro { + font-size: 1rem; + } + + .pathsGrid { + grid-template-columns: 1fr; + } + + .buttonGroup { + flex-direction: column-reverse; + } + + .button, .buttonSecondary { + width: 100%; + } +} diff --git a/apps/web/app/onboarding/page.tsx b/apps/web/app/onboarding/page.tsx new file mode 100644 index 00000000..b8c18915 --- /dev/null +++ b/apps/web/app/onboarding/page.tsx @@ -0,0 +1,39 @@ +/** + * Onboarding Flow + * Protected route - requires authentication + */ + +import { redirect } from 'next/navigation'; +import { cookies } from 'next/headers'; +import { verifySession } from '@/lib/auth/session'; +import { findUserById } from '@/lib/db/users'; +import OnboardingClient from './OnboardingClient'; + +export default async function OnboardingPage() { + // Get session from cookies + const cookieStore = await cookies(); + const sessionToken = cookieStore.get('session')?.value; + + if (!sessionToken) { + redirect('/login'); + } + + // Verify session + const session = verifySession(sessionToken); + if (!session) { + redirect('/login'); + } + + // Get user + const user = await findUserById(session.userId); + if (!user) { + redirect('/login'); + } + + // If user already completed onboarding, redirect to dashboard + if (user.onboarding_completed_at) { + redirect('/dashboard'); + } + + return ; +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx new file mode 100644 index 00000000..8fc93a46 --- /dev/null +++ b/apps/web/app/page.tsx @@ -0,0 +1,282 @@ +/** + * TogetherOS Home Page + * + * Welcome page showcasing the UI foundation and core features + */ + +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+
+ +
+
+

+ Welcome to TogetherOS +

+

+ A cooperative operating system for collective action. Built on principles of + democracy, transparency, and shared prosperity. +

+ +
+ + + + + + + + + +
+
+
+
+ + {/* Features Section */} +
+
+

+ Eight Paths of Cooperation +

+

+ TogetherOS is organized around eight essential cooperation systems, + each designed to empower collective action and democratic decision-making. +

+
+ +
+ {/* Path 1: Groups */} + + + Groups & Organizations + + Form collectives, manage membership, and coordinate activities + + + +

+ Create and join groups with democratic governance structures. + Manage roles, permissions, and collaborative workflows. +

+
+ + + +
+ + {/* Path 2: Forum */} + + + Forum & Deliberation + + Structured discussions with voting and consensus tools + + + +

+ Engage in meaningful dialogue with built-in moderation, + threading, and democratic decision-making features. +

+
+ + + +
+ + {/* Path 3: Proposals */} + + + Proposals & Governance + + Democratic decision-making with transparent voting + + + +

+ Submit proposals, discuss amendments, and vote on collective + decisions with full transparency and accountability. +

+
+ + + +
+ + {/* Path 4: Bridge */} + + + AI Bridge + + Cooperative AI assistance for knowledge and tasks + + + +

+ Interact with AI assistants trained on cooperative principles, + helping groups make informed decisions together. +

+
+ + + + + +
+ + {/* Path 5: Resources */} + + + Resource Sharing + + Commons-based resource allocation and management + + + +

+ Share tools, spaces, and resources. Track usage, + coordinate access, and ensure equitable distribution. +

+
+ + + +
+ + {/* Path 6: Economic */} + + + Economic Commons + + Transparent finances and value distribution + + + +

+ Manage shared funds, track contributions, and distribute + value according to democratic principles. +

+
+ + + +
+ + {/* Path 7: Knowledge */} + + + Knowledge Commons + + Collaborative documentation and learning + + + +

+ Build shared knowledge bases, create learning pathways, + and document collective wisdom. +

+
+ + + +
+ + {/* Path 8: External */} + + + External Relations + + Connect and collaborate across organizations + + + +

+ Build networks, establish partnerships, and coordinate + activities across multiple cooperatives. +

+
+ + + +
+
+
+ + {/* CTA Section */} +
+
+

+ Ready to Start Cooperating? +

+

+ Join the movement for democratic, transparent, and cooperative technology. +

+
+ + + + + + +
+
+
+ + {/* Footer */} +
+
+
+

+ © 2025 TogetherOS. Built with cooperation in mind. +

+
+ + Design System + + + Status + + + AI Bridge + +
+
+
+
+
+ ); +} diff --git a/apps/web/app/profile/ProfileClient.tsx b/apps/web/app/profile/ProfileClient.tsx new file mode 100644 index 00000000..2043881e --- /dev/null +++ b/apps/web/app/profile/ProfileClient.tsx @@ -0,0 +1,402 @@ +'use client'; + +import { useState, FormEvent } from 'react'; +import { useRouter } from 'next/navigation'; +import styles from './profile.module.css'; + +interface User { + id: string; + email: string; + name?: string; + username?: string; + bio?: string; + avatar_url?: string; + city?: string; + state?: string; + country?: string; + paths?: string[]; + skills?: string[]; + can_offer?: string; + seeking_help?: string; +} + +const COOPERATION_PATHS = [ + { id: 'education', name: 'Collaborative Education', emoji: '📚' }, + { id: 'economy', name: 'Social Economy', emoji: '💰' }, + { id: 'wellbeing', name: 'Common Wellbeing', emoji: '🫶' }, + { id: 'technology', name: 'Cooperative Technology', emoji: '💻' }, + { id: 'governance', name: 'Collective Governance', emoji: '🏛️' }, + { id: 'community', name: 'Community Connection', emoji: '🤝' }, + { id: 'media', name: 'Collaborative Media', emoji: '🎨' }, + { id: 'planet', name: 'Common Planet', emoji: '🌍' }, +]; + +export default function ProfileClient({ initialUser }: { initialUser: User }) { + const router = useRouter(); + const [isEditing, setIsEditing] = useState(false); + const [user, setUser] = useState(initialUser); + const [formData, setFormData] = useState({ + name: user.name || '', + username: user.username || '', + bio: user.bio || '', + avatar_url: user.avatar_url || '', + city: user.city || '', + state: user.state || '', + country: user.country || '', + paths: user.paths || [], + skills: (user.skills || []).join(', '), + can_offer: user.can_offer || '', + seeking_help: user.seeking_help || '', + }); + const [state, setState] = useState<'idle' | 'saving' | 'error'>('idle'); + const [errorMessage, setErrorMessage] = useState(''); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setState('saving'); + setErrorMessage(''); + + try { + const response = await fetch('/api/profile', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + skills: formData.skills + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }), + }); + + const data = await response.json(); + + if (!response.ok) { + setState('error'); + setErrorMessage(data.error || 'Failed to update profile'); + return; + } + + setUser(data.user); + setState('idle'); + setIsEditing(false); + } catch (error) { + console.error('Update error:', error); + setState('error'); + setErrorMessage('Failed to update. Please try again.'); + } + }; + + const togglePath = (pathId: string) => { + const newPaths = formData.paths.includes(pathId) + ? formData.paths.filter((p) => p !== pathId) + : [...formData.paths, pathId]; + setFormData({ ...formData, paths: newPaths }); + }; + + if (!isEditing) { + // View Mode + return ( +
+
+
+

Your Profile

+
+ + +
+
+
+ +
+
+ {user.avatar_url && ( + Avatar + )} + +
+

Basic Info

+
+ +

{user.email}

+
+ {user.name && ( +
+ +

{user.name}

+
+ )} + {user.username && ( +
+ +

@{user.username}

+
+ )} + {user.bio && ( +
+ +

{user.bio}

+
+ )} +
+ + {(user.city || user.state || user.country) && ( +
+

Location

+

+ {[user.city, user.state, user.country].filter(Boolean).join(', ')} +

+
+ )} + + {user.paths && user.paths.length > 0 && ( +
+

Your Cooperation Paths

+
+ {COOPERATION_PATHS.filter((p) => user.paths?.includes(p.id)).map((path) => ( +
+ {path.emoji} + {path.name} +
+ ))} +
+
+ )} + + {user.skills && user.skills.length > 0 && ( +
+

Skills

+
+ {user.skills.map((skill, i) => ( + + {skill} + + ))} +
+
+ )} + + {user.can_offer && ( +
+

What I Can Offer

+

{user.can_offer}

+
+ )} + + {user.seeking_help && ( +
+

What I'm Seeking

+

{user.seeking_help}

+
+ )} +
+
+
+ ); + } + + // Edit Mode + return ( +
+
+
+

Edit Profile

+
+ +
+
+
+ +
+
+
+

Basic Info

+ +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Your name" + className={styles.input} + /> +
+ +
+ + setFormData({ ...formData, username: e.target.value })} + placeholder="username" + className={styles.input} + /> +

3-50 characters, letters, numbers, underscores, hyphens

+
+ +
+ +