diff --git a/.gitattributes b/.gitattributes index f87b33e0..31305ab0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ scripts/docker-entrypoint.sh text eol=lf +docs/examples/operations/*.sh text eol=lf **/deft.app.lock.json text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 127cfd4b..01523549 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,8 @@ jobs: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile + - name: Check public documentation and tutorial manifest + run: pnpm docs:check - name: Verify bundled module provenance run: pnpm module:verify - name: Test App Kit authoring contracts diff --git a/FEATURES.md b/FEATURES.md index c74db7e1..be986c16 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -180,7 +180,8 @@ Fresh installs use `init` (`pnpm db:push-full` plus platform seed). Supported ve ## Apps and Modules -- Modules define domain records, relationships, and Deft-rendered native views. +- Modules define domain records, relationships, and Deft-rendered native views. Standalone manifests install through Settings → Modules without the Apps feature flags. +- Module v1 data is shared with owners, admins, and members; it has no private rows or fields. [Choose between a Module and an App](docs/modules-and-apps.md). - Declarative internal Apps package Modules for review and installation and are an opt-in alpha capability. - Connected Apps and bounded daily actions are included in preview.15. They are experimental, disabled by default, and require the flags and review flow in the [operator guide](docs/app-run-operations.md). - The current App protocols do not provide arbitrary custom UI, public portals, general external runtimes, or synchronization. diff --git a/README.md b/README.md index 88e28e47..83033b50 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,13 @@ Use `init` only with a fresh database. For custom ports, public URLs, image veri [Architecture](#architecture) · [Current limitations](docs/current-limitations.md) · [Licensing](#license) +## Try one workflow + +- [Set up your first workspace](docs/tutorials/first-workspace.md) +- [Ask Defty for a task and approve it](docs/tutorials/first-agent-action.md) +- [Connect a personal AI client](docs/tutorials/first-mcp-connection.md) +- [Build a small vendor Module](docs/tutorials/first-module.md) + ## The core loop 1. **Capture the discussion.** Save useful decisions and references in Knowledge with links back to their source conversation. @@ -78,7 +85,7 @@ Optional video chapters: [Knowledge capture — 1:09](https://youtu.be/7z9EH4c9k ### A workspace people can use normally - Real-time chat with spaces, DMs, threads, mentions, reactions, files, presence, and rich text -- Task management with Board, Table, Timeline, Calendar, Pipeline, and personal views +- Task management with Board, Table, Timeline, Calendar, and personal views - Notes, company knowledge, channel memory, decisions, references, and knowledge graph views - Native calendar events plus read-only ICS subscriptions - Dashboard, inbox, notifications, people, teams, roles, and profile management @@ -97,7 +104,7 @@ Deft still works as a normal workspace without an AI provider key. Chat, tasks, ### An extensible workspace through Modules and Apps -Modules add domain records, relationships, and native views. Apps package supported workspace extensions for operator review and installation. Declarative internal Apps, connected Apps, and bounded scheduled actions are opt-in alpha capabilities and remain disabled by default. Arbitrary custom UI and public portals are planned rather than part of the current contract. +Modules add domain records, relationships, and native views. Apps package Module resources and may request supported connected actions. They use different manifests and installation flows; start with [Modules and Apps](docs/modules-and-apps.md). Declarative internal Apps, connected Apps, and bounded scheduled actions are opt-in alpha capabilities and remain disabled by default. Arbitrary custom UI and public portals are planned rather than part of the current contract. The bundled **Contacts** module is the first example of this model. The goal is not to turn Deft's core into every application a company might need, but to let new capabilities live on the same shared substrate instead of becoming another disconnected system. @@ -111,7 +118,7 @@ Chat is both a human communication surface and part of the workspace record. Thr ### Tasks turn context into accountable work -Projects support Board, Table, Timeline, Calendar, and Pipeline views, plus dependencies, subtasks, recurrence, comments, activity diffs, bulk actions, and agent-created drafts. +Projects support Board, Table, Timeline, and Calendar views, plus dependencies, subtasks, recurrence, comments, activity diffs, bulk actions, and agent-created drafts. ![Deft task table](docs/assets/repository/tasks-table.png) diff --git a/RELEASING.md b/RELEASING.md index dc78c87a..450bb03c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -152,3 +152,7 @@ Historical tags through `v0.2.0-preview.4` retain BSL 1.1 as shipped. Do not rewrite those tags. GitHub's source archive plus the repository's build and installation scripts are the Corresponding Source offered with the official image. + +## Documentation gate + +Complete [the documentation release checks](docs/documentation-maintenance.md#before-a-release) before publication. Keep the website and README on the same recommended installation path, and record install, restore, upgrade, MCP, and Module evidence for the release. diff --git a/docs/README.md b/docs/README.md index a088268a..ea1bade1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,8 +13,22 @@ - [Security](../SECURITY.md) - [Contributing](../CONTRIBUTING.md) +## Learn by doing + +- [Your first workspace](tutorials/first-workspace.md) +- [Your first approved agent action](tutorials/first-agent-action.md) +- [Connect your first AI client](tutorials/first-mcp-connection.md) +- [Build your first Module](tutorials/first-module.md) +- [Create a task through MCP](tutorials/mcp-task-example.md) + +The tutorials target `v0.3.0-preview.15`. Their examples live in +[`docs/examples/`](examples/README.md). Use the website for navigation and search, +and the matching release's source for operational contracts. + ## Operator and integration guides +- [Modules and Apps: choose a starting point](modules-and-apps.md) + - [Connected App author guide](connected-app-author-guide.md) - [Historical Hermes compatibility guide](../integrations/hermes/deft-platform/README.md) (use only with a release that explicitly certifies it; excluded from the preview.15 core release) - [Legacy Hermes Agent Channel bridge rollback](hermes-agent-channel-service.md) diff --git a/docs/connected-app-author-guide.md b/docs/connected-app-author-guide.md index e16a1dc3..3cb7f1dd 100644 --- a/docs/connected-app-author-guide.md +++ b/docs/connected-app-author-guide.md @@ -2,6 +2,8 @@ > **Experimental in preview.15:** this connected-App and bounded-automation flow is included in the published `v0.3.0-preview.15` core release. It is experimental and disabled by default. +For a standalone tracker, use the [Module tutorial](tutorials/first-module.md). A `deft.module.json` installs through Settings → Modules; this guide packages resources in a separate `deft.app.json` and uses Settings → Apps. [Compare the two paths](modules-and-apps.md). + This guide shows how to build a connected App, check that it matches the host, and submit it for workspace review. It supports App Protocol v1 connected Apps and the bounded scheduling requests in Protocol v2. The workspace operator diff --git a/docs/current-limitations.md b/docs/current-limitations.md index d13f09e0..52636fb8 100644 --- a/docs/current-limitations.md +++ b/docs/current-limitations.md @@ -49,6 +49,8 @@ Deft is an alpha. It is suitable for technical evaluation, internal use, and con ## Apps and Modules +[Modules and Apps](modules-and-apps.md) use different packaging and installation paths. Standalone Modules do not require the experimental Apps flags. Module v1 records are organization-wide for owners, admins, and members; guests are denied, and private rows, collections, and fields are not supported. + - Apps are disabled by default. The API requires `DEFT_APPS_ENABLED=true`, and the web build requires `NEXT_PUBLIC_FEATURE_APPS=true`. - Connected App execution and bounded automations add further flags and operator review. They are included in preview.15, experimental, and disabled by default. - App Protocol v0 provides declarative internal records and native views. Current protocols do not provide arbitrary custom UI, public portals, general external runtimes, or synchronization. diff --git a/docs/documentation-maintenance.md b/docs/documentation-maintenance.md new file mode 100644 index 00000000..e0abdcc4 --- /dev/null +++ b/docs/documentation-maintenance.md @@ -0,0 +1,36 @@ +# Keeping documentation current + +## Ownership + +Keep deployment contracts, schemas, runnable examples, and tutorial source in this repository. The website presents those instructions with search and navigation. Historical plans and audits stay under `docs/superpowers/`; they are not setup guides. + +The tutorial Markdown in `docs/tutorials/` is canonical. The website's `scripts/sync-product-docs.mjs` copies it into the documentation layout and copies the example downloads. Run the script with a checkout containing the reviewed documentation changes, then build and check the website. Do not maintain two independent tutorial drafts. + +## Writing + +- Lead with what the reader will accomplish. +- Name the starting state, required role, and relevant release. +- Use actual UI labels and commands from that release. +- Put a short expected result after each important action. +- Explain the likely failure next to the step it affects. +- Prefer one example over a long feature list. Link to reference details. +- Label illustrative output and demo screenshots. Do not imply they came from the reader's workspace. + +## Before a release + +1. Check the image tag and digest, downloaded asset names, and required secrets. +2. Follow the recommended fresh-install path in a disposable deployment. +3. Create a task and attachment. Back up, restore into a separate project, and verify both. +4. Rehearse the supported upgrade from its declared baseline. Record the previous and target image digests. +5. Check personal MCP scopes, an authenticated read, a write, and a retry with the same idempotency key. +6. Validate the example Module, install it, create its records, and test its upgrade. +7. Update changed limits, flags, client compatibility, and runtime certification statements. +8. Sync the website tutorials, build it, check links, and inspect desktop and mobile navigation. + +Run `pnpm docs:check` for local links, the Module example, and operational scripts with a simulated Docker command. The tests cover restart behavior, failure handling, legacy uploads, checksums, target collisions, and successful restore commands. They require Bash and do not replace a real install or restore test. + +In the website checkout, run `pnpm docs:sync --check /path/to/Deft` to compare against the reviewed source. The normal website build also checks synced-file integrity, release consistency, search behavior, rendered command/download parity, and local links. Synchronization refuses a tutorial that targets a different release from the site configuration. + +## Record the evidence + +For each workflow, record the release/commit, environment, command or user actions, result, and remaining limitation. Say “targets this release” when only source was checked. Reserve “verified” for an actual recorded run. Update the website release badge only after the release exists and its instructions are reviewed. diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 00000000..9ff93d77 --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,17 @@ +# Documentation examples + +These examples target `v0.3.0-preview.15`. Use fictional data in a disposable workspace. + +- `vendor-pilot/deft.module.json`: a two-collection Module for the [Module tutorial](../tutorials/first-module.md). +- `operations/backup-release.sh`: a backup example for a release-image deployment using Docker named volumes. It also saves legacy container uploads when present. Run `bash backup-release.sh` from the release folder, or add `--leave-stopped` before an upgrade. +- `operations/restore-release.sh`: a restore rehearsal in a new Compose project. Export `RECOVERY` (the absolute recovery-folder path) and `DEFT_RESTORE_IMAGE` (the saved immutable image digest) before running it. It refuses an existing target project or directory. Optional `RESTORE_PROJECT` and `RESTORE_DIR` choose other names; defaults are `deft-docs-restore` and `deft-restore`. + +The Bash examples are for Linux. On Windows, use WSL with Docker integration; Git Bash needs its Docker path-conversion behavior accounted for. Rehearse on an isolated host because restored jobs and configured integrations can resume with the app. + +Before upgrading an older deployment, copy captured legacy uploads into the persistent volume as described in [self-hosting](../self-hosting.md#upgrading). Restoring an older image also requires its original upload path and any source-mounted files; the restore example targets preview.15. + +Validate the manifest from the repository root: + +```bash +pnpm module:check docs/examples/vendor-pilot +``` diff --git a/docs/examples/operations/backup-release.sh b/docs/examples/operations/backup-release.sh new file mode 100644 index 00000000..20625610 --- /dev/null +++ b/docs/examples/operations/backup-release.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 +leave_stopped=false +case "${1:-}" in + '') ;; + --leave-stopped) leave_stopped=true; shift ;; + --help) echo 'Usage: bash backup-release.sh [--leave-stopped]'; exit 0 ;; + *) echo 'Usage: bash backup-release.sh [--leave-stopped]' >&2; exit 1 ;; +esac +test "$#" -eq 0 || { echo 'Unexpected arguments.' >&2; exit 1; } +for tool in docker gzip sha256sum find sort xargs; do + command -v "$tool" >/dev/null || { echo "Missing $tool. Run this example in Linux or WSL with Docker integration." >&2; exit 1; } +done +dc() { docker compose -f docker-compose.yml -f compose.prod.yml -f compose.release.yml "$@"; } +mkdir -p "$PWD/backups" +RECOVERY="$(mktemp -d "$PWD/backups/deft-recovery-$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX")" +container_id="$(dc ps -q deft)" +test -n "$container_id" || { echo 'Start from the running deployment.'; exit 1; } +uploads_volume="$(docker inspect "$container_id" --format '{{range .Mounts}}{{if eq .Destination "/app/uploads"}}{{.Name}}{{end}}{{end}}')" +test -n "$uploads_volume" || { echo 'Use the backup procedure for your storage mount.'; exit 1; } +docker volume inspect "$uploads_volume" >/dev/null +legacy_uploads="$(docker exec "$container_id" sh -c 'if [ -d /app/apps/api/uploads ]; then printf present; fi')" +dc stop deft +dc exec -T postgres pg_dump -U postgres --clean --if-exists --no-owner --no-privileges deft \ + | gzip -9 > "$RECOVERY/database.sql.gz" +docker run --rm -v "$uploads_volume:/source:ro" -v "$RECOVERY:/backup" alpine:3.22 \ + tar -C /source -czf /backup/uploads.tar.gz . +if [ "$legacy_uploads" = present ]; then + mkdir "$RECOVERY/legacy-container-uploads" + docker cp "$container_id:/app/apps/api/uploads/." "$RECOVERY/legacy-container-uploads/" +fi +docker inspect "$container_id" --format '{{.Image}}' > "$RECOVERY/running-image-id.txt" +docker image inspect "$(cat "$RECOVERY/running-image-id.txt")" --format '{{json .RepoDigests}}' \ + > "$RECOVERY/running-image-repo-digests.json" +cp .env docker-compose.yml compose.prod.yml compose.release.yml "$RECOVERY/" +for file in release-manifest.json SHA256SUMS; do + if [ -f "$file" ]; then cp "$file" "$RECOVERY/release-$file"; fi +done +(cd "$RECOVERY" && find . -type f ! -name SHA256SUMS -print0 \ + | sort -z | xargs -0 sha256sum > SHA256SUMS) +printf 'Recovery folder: %s\n' "$RECOVERY" +if "$leave_stopped"; then + echo 'Backup complete. Deft remains stopped for the upgrade.' +else + dc start deft + echo 'Backup complete. Deft restarted.' +fi diff --git a/docs/examples/operations/restore-release.sh b/docs/examples/operations/restore-release.sh new file mode 100644 index 00000000..76e4e784 --- /dev/null +++ b/docs/examples/operations/restore-release.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 +for tool in docker gunzip sha256sum; do + command -v "$tool" >/dev/null || { echo "Missing $tool. Run this example in Linux or WSL with Docker integration." >&2; exit 1; } +done +: "${RECOVERY:?Set RECOVERY to the absolute path of your recovery folder}" +: "${DEFT_RESTORE_IMAGE:?Set DEFT_RESTORE_IMAGE to the digest saved in running-image-repo-digests.json}" +[[ "$RECOVERY" = /* ]] || { echo 'RECOVERY must be an absolute path.'; exit 1; } +[[ "$DEFT_RESTORE_IMAGE" =~ ^ghcr\.io/maneek21/deft@sha256:[a-f0-9]{64}$ ]] || { echo 'Use the saved Deft image digest.'; exit 1; } +(cd "$RECOVERY" && sha256sum -c SHA256SUMS) +RESTORE_PROJECT="${RESTORE_PROJECT:-deft-docs-restore}" +RESTORE_DIR="${RESTORE_DIR:-deft-restore}" +[[ "$RESTORE_PROJECT" =~ ^[a-z0-9][a-z0-9_-]*$ ]] || { echo 'RESTORE_PROJECT must use lowercase letters, digits, hyphens, or underscores.'; exit 1; } +test ! -e "$RESTORE_DIR" || { echo 'RESTORE_DIR already exists. Choose a new directory.'; exit 1; } +export COMPOSE_PROJECT_NAME="$RESTORE_PROJECT" +containers="$(docker ps -aq --filter "label=com.docker.compose.project=$COMPOSE_PROJECT_NAME")" +volumes="$(docker volume ls -q --filter "name=^${COMPOSE_PROJECT_NAME}_(pgdata|uploads)$")" +test -z "$containers$volumes" || { echo 'This restore project already has containers or volumes. Set RESTORE_PROJECT to an unused name.'; exit 1; } +mkdir "$RESTORE_DIR" +cd "$RESTORE_DIR" +cp "$RECOVERY/.env" "$RECOVERY/docker-compose.yml" "$RECOVERY/compose.prod.yml" "$RECOVERY/compose.release.yml" . +export DEFT_IMAGE="$DEFT_RESTORE_IMAGE" +export DEFT_BIND_HOST=127.0.0.1 +export DEFT_WEB_PORT=127.0.0.1:3400 +export DEFT_API_PORT=127.0.0.1:3401 +export DEFT_POSTGRES_PORT=55432 +export NEXT_PUBLIC_APP_URL=http://localhost:3400 +export NEXT_PUBLIC_API_URL=http://localhost:3401 +export NEXT_PUBLIC_WS_URL=http://localhost:3401 +# Keep the rehearsal settings when this shell exits. Compose uses the last value. +cat >> .env < Create a task in Launch pilot called “Review the launch announcement”. Assign it to me and make it due tomorrow. Include this checklist in the description: check the date, test the demo link, and confirm the support contact. + +If Defty asks which project or person you mean, select the intended match. It should draft the task rather than guess between similar names. + +## 2. Review the proposal + +Wait for the approval card. Check the title, project, assignee, due date, and description before choosing **Approve**. Pay attention to the actual calendar date when you used “tomorrow”. + +If something is wrong, reject the proposal and send a corrected request. A pending proposal is not a completed task. + +## 3. Check what changed + +Open **Launch pilot** and find **Review the launch announcement**. Confirm its owner, date, and checklist match the approved proposal. Read the completed action or receipt in Deft's activity history and follow the task link when one is shown. + +**Success means both are present:** the saved task and the completed action record. A reply saying “done” alone is not enough. + +For a recorded example, watch [the task-and-approval chapter](https://youtu.be/7z9EH4c9k2o?t=126). The video uses seeded demo data; your task and project names will differ. + +## 4. Try rejecting a proposal + +Ask Defty to create another task named **Approval practice — reject this**. Reject its card, then check that the task was not created. This gives you a simple check of both outcomes before assigning real work. + +## If it does not work + +| What happened | What to check | +|---|---| +| No response | Confirm the provider works and Defty was selected from mention autocomplete. | +| Task appeared without review | Check the workspace trust policy; confirm the request went to Defty rather than a personal MCP client. | +| Approval exists but no task appears | Check the action status and error details before submitting the request again. | +| Wrong project or owner | Reject the draft and use the exact project name or person's email in the next request. | + +See [Approval rails](https://deft.ing/docs/approval-rails/) for policy details. To let a personal AI client use the workspace, follow [Connect your first AI client](first-mcp-connection.md). diff --git a/docs/tutorials/first-mcp-connection.md b/docs/tutorials/first-mcp-connection.md new file mode 100644 index 00000000..ca4f9682 --- /dev/null +++ b/docs/tutorials/first-mcp-connection.md @@ -0,0 +1,39 @@ +# Connect your first AI client + +For Deft v0.3.0-preview.15. + +## Choose the identity + +A personal MCP connection acts as **you**. It can access only what your account and granted scopes allow. It does not create an agent employee. + +Start with read access. Personal write-enabled connections can change data without entering Defty's approval queue. + +## 1. Create the connection + +Sign in to Deft and open **Settings → MCP Access**. Choose the client you use and follow its connection instructions. Prefer OAuth when supported; otherwise create a scoped personal token and store it in the client's credential settings. + +The endpoint is `https://your-deft.example.com/api/mcp/v1`. Use the URL shown by your installation. A hosted client needs a reachable HTTPS deployment; it cannot access a server running only on your laptop's localhost. + +Grant `read:workspace` and `read:tasks` for this exercise. See the guides for [Claude.ai](https://deft.ing/docs/claude-online/), [Claude Desktop](https://deft.ing/docs/claude-desktop/), [Claude Code](https://deft.ing/docs/claude-code/), or [Cursor and Codex](https://deft.ing/docs/cursor-codex/) for client-specific setup. + +## 2. Test a read + +Ask the client: + +> List my tasks in Deft. Include each task's title, status, and task key. Do not change anything. + +Compare one result with the same task in Deft. If you followed the workspace tutorial, look for **Draft the launch checklist**. + +**Check:** the client returns real tasks you can open, with matching details. An empty result may be correct if your account has no assigned tasks or cannot access that project. + +## 3. Add writes only when needed + +If you want task creation, add `write:tasks` through the connection's supported grant flow. Then ask for one clearly named test task in a specific project and verify it in Deft. The write runs with your authority; it is not waiting for an employee approval card. + +Developers can follow the [MCP task example](mcp-task-example.md) for exact tool arguments and retry behavior. + +## 4. Revoke the connection + +Return to **Settings → MCP Access** and revoke the test connection when finished. Try another task read from the client: it should require a new authorization or fail authentication. + +If the connection fails, check the endpoint, token or OAuth grant, required scopes, and network reachability. Keep tokens out of prompts and support screenshots. diff --git a/docs/tutorials/first-module.md b/docs/tutorials/first-module.md new file mode 100644 index 00000000..631d1c5c --- /dev/null +++ b/docs/tutorials/first-module.md @@ -0,0 +1,68 @@ +# Build your first Module + +For Deft v0.3.0-preview.15. + +## Before you start + +You need a preview.15 workspace, an owner or admin account, Node.js 22.13 or newer, and pnpm 11.10.0. This tutorial creates a declarative Module with vendors and related services. It does not need the experimental App Kit or a provider connection. + +Module records are visible to ordinary workspace members. Use the fictional sample data below, not private business records. + +## 1. Scaffold the Module + +Run from a parent directory in Bash: + +```bash +git clone --branch v0.3.0-preview.15 --depth 1 https://github.com/Maneek21/Deft.git deft-module-source +cd deft-module-source +pnpm install --frozen-lockfile +pnpm module:init ../deft-module-vendor-pilot +``` + +The destination must be new or empty. The scaffold creates `deft.module.json` and supporting authoring files. + +## 2. Add vendors and services + +Download the [complete example manifest](../examples/vendor-pilot/deft.module.json) and save it over `../deft-module-vendor-pilot/deft.module.json`. + +The manifest declares two collections: + +| Collection | Fields | Purpose | +|---|---|---| +| Vendors | Required name, email, notes | Stores each vendor once. | +| Services | Required name, vendor relation | Links a service to its vendor. | + +Each collection has Table, Form, and Details views. `search.title_field` identifies the display name; `search.fields` lists the fields to search. The relation's `target_collection: "vendors"` points to the Vendors collection. + +The Module id is `community.example.vendor-pilot`, its slug is `vendor-pilot`, and its first version is `0.1.0`. Keep the id and slug unchanged when upgrading it. + +## 3. Validate it + +Run from the Deft source directory: + +```bash +pnpm module:format ../deft-module-vendor-pilot +pnpm module:check ../deft-module-vendor-pilot +``` + +The check should exit successfully and print a digest. If it reports a bad reference, compare every view field and relation target with the keys declared in the manifest. Fix the reported problem before installation. + +## 4. Install and create sample records + +Open **Settings → Modules**, choose **Install local**, and upload `deft.module.json`. Review the identity and collections, then choose **Confirm install**. + +Open **Vendor pilot** in workspace navigation. Create a vendor named **Example Studio** with email `hello@example.com`. In Services, create **Launch illustrations**, then open the saved record. Under **Related records → Vendor**, choose **Edit**, select **Example Studio**, and **Save**. Relations are added after record creation. + +**Check:** both records appear in their collections, and the service links to the correct vendor. To link a task, open **Draft the launch checklist** from the workspace tutorial and use its **References** tab to attach **Example Studio**. Return to the vendor record and confirm the task appears under **Linked tasks**. + +Agent access starts at `none`. Leave it there for this exercise. This setting controls Defty and agent employees. Personal MCP connections use your account's permissions and need the corresponding Module scopes. + +## 5. Try a small upgrade + +Change the manifest version to `0.1.1` and change its description. Format and check it again, then choose **Update local manifest** on its card in Settings → Modules. Upload the revised file, check the version and digest against the CLI output, and choose **Confirm update**. + +**Check:** the active version is `0.1.1` and both sample records remain. Larger field changes must also validate the existing records; changing the schema does not make incompatible data disappear. + +## Next + +Read the [Module reference](https://deft.ing/docs/modules/) for supported fields, views, and access limits. [Modules and Apps](../modules-and-apps.md) explains the separate package and review flow for supported connected actions. diff --git a/docs/tutorials/first-workspace.md b/docs/tutorials/first-workspace.md new file mode 100644 index 00000000..fc3093e3 --- /dev/null +++ b/docs/tutorials/first-workspace.md @@ -0,0 +1,51 @@ +# Your first workspace + +For Deft v0.3.0-preview.15. + +## Start with a fresh workspace + +Complete [Quick start](https://deft.ing/docs/quick-start/) and sign in as the owner. You can follow this tutorial without an AI provider. Use the sample names below so the next tutorial has something to work with. + +## 1. Create a space + +Use **Create space** beside Spaces in the sidebar. Name it **launch-room** and choose public visibility so invited workspace members can find it. + +Post this message: + +> We need a short launch checklist. It should cover the announcement, the demo, and the support handoff. + +**Check:** the message appears in `launch-room` under your name. Public means visible within your workspace; it does not publish the conversation on the web. + +## 2. Create a project + +Use **Create project** beside Projects. Name it **Launch pilot** and enter **PILOT** in **Prefix**. Keep the default workflow for this exercise. + +**Check:** the project appears in the sidebar and its name appears above the task view. If the previous project is still shown, select **Launch pilot** in the sidebar and refresh before adding a task. + +## 3. Add a task + +Open the project and choose **New task**. Enter: + +| Field | Value | +|---|---| +| Title | Draft the launch checklist | +| Assignee | The signed-in owner account | +| Priority | P2 | +| Due date | Tomorrow's date in the date picker | +| Description | Cover the announcement, demo, and support handoff. | + +Save it, then open the task. Check the project, assignee, and description. Confirm the saved due date matches the date you selected. Its task key starts with `PILOT-`; use the actual key when referring to it later. + +Move the task to **In progress** in Board. Switch to Table and confirm it has the same status. Both views show the same task. + +## 4. Invite a teammate + +Open **Settings → Members → Invite**, enter their email and role, and generate an invitation link. Share the link directly; Deft does not email it for you. + +Ask the teammate to open `launch-room` and reply to your message. Invitation links expire after seven days and work once. See [First login](https://deft.ing/docs/first-login/) for recovery and administrator setup. + +## You are ready for real work + +You have a conversation, a project, and an assigned task. Keep them for [your first approved agent action](first-agent-action.md). + +If a teammate cannot find the space, check its visibility and their membership. If a task seems missing, check the selected project and clear filters before creating it again. diff --git a/docs/tutorials/mcp-task-example.md b/docs/tutorials/mcp-task-example.md new file mode 100644 index 00000000..24d368df --- /dev/null +++ b/docs/tutorials/mcp-task-example.md @@ -0,0 +1,60 @@ +# Create a task through MCP + +For Deft v0.3.0-preview.15. + +## Connection and scopes + +Use an authenticated personal MCP client connected to `/api/mcp/v1`, with `read:workspace`, `read:tasks`, and `write:tasks`. These examples are the JSON-RPC request bodies sent by an initialized MCP client, not standalone HTTP setup instructions. + +Call `tools/list` first. The live catalog for your release defines the exact accepted arguments. Personal writes act as the connected human and do not wait in the employee approval queue. + +## 1. Resolve the project + +```json +{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"resolve_project","arguments":{"query":"Launch pilot"}}} +``` + +Read the tool's content. Continue only when it identifies one resolved project. If it returns `ambiguous`, choose from the candidates; if `not_found`, check the project name and your access. Use the returned project id below. + +## 2. Create one task + +Replace `PROJECT_ID_FROM_RESOLVER` before sending: + +```json +{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"task_create","arguments":{"project_id":"PROJECT_ID_FROM_RESOLVER","title":"Review the launch announcement","description":"Check the date, demo link, and support contact.","priority":"p2","idempotency_key":"launch-pilot-announcement-001"}}} +``` + +The request leaves the task unassigned. To assign it, resolve a member first and pass the returned `assignee_id`. + +The successful result includes the saved task fields, an `id`, and `task_key`. Keep those actual values; do not infer an id from the title. The JSON-RPC request id only matches a response to a request. `idempotency_key` identifies the write you may need to retry. + +## 3. Read it back + +```json +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"task_get","arguments":{"task_id":"TASK_ID_FROM_CREATE"}}} +``` + +The tool's text content contains JSON with a `task` object, `recent_comments`, and `recent_activity`. Compare `task.title`, `task.description`, and `task.project_id` with the request. Open `task.task_key` in Deft to check the same record in the UI. + +## Arguments used here + +| Tool | Required arguments | Scope | +|---|---|---| +| `resolve_project` | `query` | `read:workspace` | +| `task_create` | `title`; pass the resolved `project_id` explicitly | `write:tasks` | +| `task_get` | `task_id` | `read:tasks` | + +For `task_create`, the example also uses `description`, `priority`, and `idempotency_key`. Priorities are lowercase `p0`, `p1`, `p2`, or `p3`. Optional `due_date` and `start_date` accept date strings; use an explicit ISO date or timestamp when dates matter. + +## Handle failures without duplicating work + +| Result | Next step | +|---|---| +| Authentication rejected | Reauthorize or replace the revoked/expired token. | +| Missing scope | Update the grant; retry only after it includes the required scope. | +| Tool result has `isError: true` | Read its message; an HTTP or JSON-RPC success alone does not mean the task was created. | +| Ambiguous project or member | Resolve the target before a write. | +| Connection drops after creation | Read the result if known; otherwise retry the same arguments with the same idempotency key. | +| Different task requested | Use a new idempotency key for the new intent. | + +Employee-token clients have a different catalog and approval behavior. Read [MCP tools](https://deft.ing/docs/mcp-tools/) before adapting this example to an employee runtime. diff --git a/package.json b/package.json index ac875fcf..6b09830a 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,8 @@ "test:upgrade": "pnpm --filter @deft/db test:upgrade && tsx --test scripts/selfhost-upgrade.test.ts", "test:release-workflow": "node --test scripts/release-workflow.test.mjs scripts/app-platform-certification-workflow.test.mjs", "test:public-env": "node --test scripts/inject-public-env.test.mjs", - "typecheck": "pnpm -r --parallel typecheck" + "typecheck": "pnpm -r --parallel typecheck", + "docs:check": "node scripts/check-public-docs.mjs && node --test scripts/docs-operations.test.mjs && pnpm module:check docs/examples/vendor-pilot" }, "engines": { "node": ">=22.13.0" diff --git a/scripts/check-public-docs.mjs b/scripts/check-public-docs.mjs new file mode 100644 index 00000000..28ee1189 --- /dev/null +++ b/scripts/check-public-docs.mjs @@ -0,0 +1,28 @@ +import { readFile, readdir, stat } from 'node:fs/promises'; +import { resolve, dirname } from 'node:path'; +const root = process.cwd(); +const files = ['README.md','FEATURES.md','docs/README.md','docs/documentation-maintenance.md','docs/examples/README.md','docs/modules-and-apps.md','docs/product-status.md','docs/current-limitations.md','docs/getting-started.md','docs/connected-app-author-guide.md','docs/self-hosted-v1-contract.md', ...(await readdir('docs/tutorials')).filter(f=>f.endsWith('.md')).map(f=>`docs/tutorials/${f}`)]; +const errors = []; +for (const file of files) { + const text = await readFile(file,'utf8'); + for (const match of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) { + const link = match[1]; + if (/^(https?:|mailto:|#)/.test(link)) continue; + if (link.startsWith('/')) { errors.push(`${file}: web-root link does not work on GitHub: ${link}`); continue; } + const [path,fragment] = link.split('#'); + const target = resolve(dirname(resolve(root,file)),path); + try { + await stat(target); + if(fragment && target.endsWith('.md')) { + const other = await readFile(target,'utf8'); + const anchors = [...other.matchAll(/^#{1,6} (.+)$/gm)].map(m=>m[1].toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu,'').replace(/ /g,'-')); + if(!anchors.includes(fragment)) errors.push(`${file}: missing heading ${link}`); + } + } catch { errors.push(`${file}: missing target ${link}`); } + } + for(const match of text.matchAll(/```json\s*\n([\s\S]*?)```/g)) { + try { JSON.parse(match[1]); } catch { errors.push(`${file}: invalid JSON example`); } + } +} +if(errors.length){ console.error(errors.join('\n')); process.exitCode=1; } +else console.log(`Public docs checks passed: ${files.length} entry points and tutorials.`); diff --git a/scripts/docs-operations.test.mjs b/scripts/docs-operations.test.mjs new file mode 100644 index 00000000..678df045 --- /dev/null +++ b/scripts/docs-operations.test.mjs @@ -0,0 +1,121 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { resolve, join, dirname } from 'node:path'; +import { spawnSync } from 'node:child_process'; + +const bash = process.env.BASH_PATH || (process.platform === 'win32' ? 'C:/Program Files/Git/bin/bash.exe' : 'bash'); +const posix = path => process.platform === 'win32' ? path.replaceAll('\\', '/').replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`) : path; +const backup = resolve('docs/examples/operations/backup-release.sh'); +const restore = resolve('docs/examples/operations/restore-release.sh'); +const docker = `#!/usr/bin/env bash +set -eu +printf '%s\\n' "$*" >> "$TEST_LOG" +case "$*" in + *'ps -q deft'*) echo fixture-container ;; + 'inspect '*'.Mounts'*) echo fixture-uploads ;; + 'inspect '*) echo sha256:fixture ;; + 'image inspect '*) echo '["ghcr.io/maneek21/deft@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]' ;; + *'pg_dump'*) test "\${FAIL_DUMP:-0}" != 1 || exit 1; echo 'SELECT 1;' ;; + *'psql -v ON_ERROR_STOP=1'*) cat > /dev/null ;; + 'exec '*'/app/apps/api/uploads'*) if [ "\${LEGACY_UPLOADS:-0}" = 1 ]; then printf present; fi ;; + 'cp '*'/app/apps/api/uploads/.'*) printf 'legacy attachment' > "\${@: -1}/legacy.txt" ;; + *'tar -C /source'*) for mount in "$@"; do + case "$mount" in *:/backup) tar -czf "\${mount%:/backup}/uploads.tar.gz" --files-from /dev/null ;; esac + done ;; +esac +`; + +function fixture() { + const dir = mkdtempSync(join(tmpdir(), 'deft-docs-')); + mkdirSync(join(dir, 'bin')); + writeFileSync(join(dir, 'bin/docker'), docker, { mode: 0o755 }); + for (const name of ['.env', 'docker-compose.yml', 'compose.prod.yml', 'compose.release.yml']) writeFileSync(join(dir, name), '# fictional test fixture\n'); + const log = join(dir, 'docker.log'); + return { + dir, + run(script, args = [], env = {}) { + return spawnSync(bash, ['-c', 'export PATH="$PWD/bin:$PATH"; bash "$@"', '--', posix(script), ...args], { + cwd: dir, encoding: 'utf8', env: { ...process.env, TEST_LOG: posix(log), ...env }, + }); + }, + log: () => readFileSync(log, 'utf8'), + cleanup() { + assert.equal(dirname(dir), tmpdir()); + assert(dir.startsWith(join(tmpdir(), 'deft-docs-'))); + rmSync(dir, { recursive: true, force: true }); + }, + }; +} + +test('normal backup resumes; upgrade backup stays stopped and retains a verifiable recovery set', () => { + for (const args of [[], ['--leave-stopped']]) { + const f = fixture(); + try { + const result = f.run(backup, args); + assert.equal(result.status, 0, result.stderr + result.stdout); + assert(f.log().includes('stop deft')); + assert.equal(f.log().includes('start deft'), args.length === 0); + const folder = join(f.dir, 'backups', readdirSync(join(f.dir, 'backups'))[0]); + const check = spawnSync(bash, ['-c', 'sha256sum -c SHA256SUMS'], { cwd: folder, encoding: 'utf8' }); + assert.equal(check.status, 0, check.stderr); + } finally { f.cleanup(); } + } +}); + +test('failed database dump leaves the app stopped and returns failure', () => { + const f = fixture(); + try { + const result = f.run(backup, [], { FAIL_DUMP: '1' }); + assert.notEqual(result.status, 0); + assert(f.log().includes('stop deft')); + assert(!f.log().includes('start deft')); + } finally { f.cleanup(); } +}); + +test('backup preserves and checksums legacy container uploads', () => { + const f = fixture(); + try { + const result = f.run(backup, ['--leave-stopped'], { LEGACY_UPLOADS: '1' }); + assert.equal(result.status, 0, result.stderr + result.stdout); + const recovery = join(f.dir, 'backups', readdirSync(join(f.dir, 'backups'))[0]); + assert.equal(readFileSync(join(recovery, 'legacy-container-uploads/legacy.txt'), 'utf8'), 'legacy attachment'); + assert.match(readFileSync(join(recovery, 'SHA256SUMS'), 'utf8'), /legacy-container-uploads\/legacy.txt/); + } finally { f.cleanup(); } +}); + +test('restore refuses an existing directory before starting containers', () => { + const f = fixture(); + try { + assert.equal(f.run(backup).status, 0); + const recovery = join(f.dir, 'backups', readdirSync(join(f.dir, 'backups'))[0]); + mkdirSync(join(f.dir, 'already-exists')); + const result = f.run(restore, [], { RECOVERY: posix(recovery), DEFT_RESTORE_IMAGE: `ghcr.io/maneek21/deft@sha256:${'a'.repeat(64)}`, RESTORE_DIR: 'already-exists' }); + assert.notEqual(result.status, 0); + assert.match(result.stdout, /RESTORE_DIR already exists/); + assert(!f.log().includes('up -d postgres')); + } finally { f.cleanup(); } +}); + +test('restore uses the chosen target, restores data, and checks services without initialization', () => { + const f = fixture(); + try { + assert.equal(f.run(backup, ['--leave-stopped']).status, 0); + const recovery = join(f.dir, 'backups', readdirSync(join(f.dir, 'backups'))[0]); + const result = f.run(restore, [], { + RECOVERY: posix(recovery), DEFT_RESTORE_IMAGE: `ghcr.io/maneek21/deft@sha256:${'a'.repeat(64)}`, + RESTORE_DIR: 'chosen-restore', RESTORE_PROJECT: 'chosen-project', + }); + assert.equal(result.status, 0, result.stderr + result.stdout); + const env = readFileSync(join(f.dir, 'chosen-restore', '.env'), 'utf8'); + assert.match(env, /COMPOSE_PROJECT_NAME=chosen-project/); + assert.match(env, /DEFT_WEB_PORT=127\.0\.0\.1:3400/); + const log = f.log(); + assert(log.includes('psql -v ON_ERROR_STOP=1 -U postgres deft')); + assert(log.includes('chosen-project_uploads:/target')); + assert(log.includes('run --rm doctor')); + assert(log.includes('run --rm smoke')); + assert(!log.includes('run --rm init')); + } finally { f.cleanup(); } +});