Skip to content

MatteoCarrabba/openworkspace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenWorkspace

OpenWorkspace is the substrate package for a plain-filesystem personal workspace: projects are directories marked by _project/id, work-organization primitives (tasks, decisions, plans, wiki, forum, automations) are plain Markdown/TOML files in a documented layout, and every view is computed from the live tree on demand — no registry, no manifests, no hidden database. It is built for one human plus many AI agents (including agents in git worktrees) writing concurrently over iCloud and git, safe by construction: atomic writes, machine-local ID minting locks, one-immutable-file-per-message coordination. The spec of record is the PRD at ~/Documents/Personal OS/_project/wiki/OPENWORKSPACE_PRD.md — where this README and the PRD disagree, the PRD wins.

This repo is itself a tracked OpenWorkspace node. It carries its own _project/ (run projects from within this directory to see its tasks/decisions), and it is owned (kind = code) by the Personal OS project in ~/Documents via a project-graph [[owns]] edge. It lives in ~/code — outside the iCloud-synced ~/Documents — per decision-3 (git repos must not live in iCloud). The design/spec docs (OPENWORKSPACE_PRD.md, PROJECT_GRAPH.md, CODE_PROJECTS.md) stay in Personal OS's wiki, not in this repo.

Install

npm install
npm run build        # tsc → dist/ (+ copies the dashboard's index.html)
npm link             # puts `projects` on PATH (bin → dist/src/cli.js)
# or run directly:  node dist/src/cli.js <command>

Node ≥ 20. Runtime dependencies: yaml and smol-toml only.

Command tour

projects home init                 mark the current directory as a workspace (.openworkspace/)
projects home list [--all]         live-scan projects (--all includes the Dormant/Archives shelves)
projects home scan                 full scan: projects, tasks, attention counts
projects home doctor               workspace + all-projects invariant checks
projects home mint-suffix [<sfx>|--clear]   this machine's ID suffix (PRD §4.4 — set "mini" on the Mini)
projects home runner-node [<path>|--clear]  this machine's granted runner node binary (decision-1, §7.4)
projects home machine-id [<name>]           show or set this machine's id — the value automation
                                            manifests' machines = [...] declarations are matched
                                            against; setting renames the synced registry file

projects init [<path>] | new "Name"  stamp the full _project/ skeleton (README, .gitignore, id,
                                     plans/, tasks/, wiki/, decisions/, automations/, forum/);
                                     init without a path initializes the cwd (refuses outside a
                                     workspace, at the workspace root, or at a shelf root)
projects show · doctor · rename · move · lifecycle <ref> --to <active|dormant|archived>
projects reconcile [--all] [--apply] [--auto]      heal location⟷metadata drift (decision-2: metadata-as-truth)
                   [--revert <ref>] [--adopt-location <ref>]   human tiebreak for ambiguous drift

projects task create "title" [--parent 36] [--quadrant q2] [--hidden-until DATE] [--recur weekly]
projects task list [--subtasks] [--hidden] [--all] · show · edit · note · archive
projects task status <id> <todo|doing|waiting|review|done> [--force]
projects task done <id>            on a recurring task: completes the occurrence
projects task hide <id> --until DATE · recur <id> <interval|off>

projects decision new "title" · accept · list · show · supersede <id> --by <id>
projects plan show · open
projects forum announce · depart · who · open · post · show · list · inbox · resolve · archive
projects forum sweep               retention: remove own-machine stale presence, propose thread archives
projects automation apply [<name>|--all] · deactivate · list [--all] · status · prune · logs · run-now
projects import legacy [--dry-run|--apply] [<project-ref>|--all]   migrate legacy _tasks/reminders/dirchannels
projects skills sync [--dry-run|--apply] [--json]   aggregate every project's Skills/ into <ws>/.agents/skills/ and install for the runtimes
projects dashboard dev | open [--port N] [--host H] [--allow-host H]... [--cache-ttl MS] [--config <path>]   read-only dashboard (localhost-only by default; Host-header-validated; scan cache off by default)

Every read takes --json; every project-scoped verb takes --project <ref> (path, workspace-relative path, unique name, or UID; default = walk-up from cwd). Exit codes: 0 ok, 1 error, 2 canonical-resolution failure (a forum/coordination verb could not resolve the project's canonical checkout — never silently degraded).

Two write postures (PRD §6.3): records ride the branch (task/decision/plan/wiki writes are worktree-local and merge with your code), while coordination rides the machine (forum and presence verbs always resolve to the project's canonical checkout by UID, so agents in different worktrees see each other immediately).

Lifecycle is metadata-as-truth (decision-2). A project's lifecycle lives in its _project/project.toml lifecycle field (active|dormant|archived; absent ⇒ active — Matteo dropped the ongoing state 2026-06-11: an "ongoing" project is just an active one he never archives, and the active-project cap is C3's concern, not the tool's); the project's location (top-level vs Dormant Projects/ vs Archives/) is a derived, reconciled view of that field, maintained for Finder legibility. projects lifecycle <ref> --to X is metadata-primary: it writes the field (and a machine-local intent record) and then moves the folder to match. This survives iCloud sync, which has no tombstone-honest delete model and can spuriously resurrect a delete or revert a move: such a location change is now cosmetic drift a projects reconcile pass heals, not state corruption. The drag-vs-glitch tiebreaker (a human Finder-drag and an iCloud glitch are the same observable event, opposite responses) is resolved by, in order: committed git (git show HEAD:project.toml where a project is a repo — iCloud can't author a commit), then a machine-local append-only intent-log in ~/Library/Application Support/OpenWorkspace/ (outside iCloud's reach), then — when neither proves intent — propose-only: reconcile surfaces both fix commands and never guesses, so it can undo a glitch but never silently overrule a human. Reconcile also heals the iCloud record-corruption shapes: same-ID duplicates (task-50 - x 2.md, archived reversibly to _project/archive/reconcile/) and resurrected state-named subdirs (tasks/todo/ — rehomed flat, tasks/archive/ whitelisted). --apply executes the plan except ambiguous rows; --auto (the Mini's mode) restricts to the glitch-certain class. git is NOT the sync and NOT the source of truth — iCloud stays the universal file sync; git is optional, per-project version control.

Skills install from a single aggregated hub (projects skills sync). Agent skills (a directory containing SKILL.md) are authored where they belong — next to the project that owns them (<Project>/Skills/<name>/, plus OpenWorkspace's own bundled skills/<name>/). skills sync discovers every skill source across the workspace (active + shelved projects, via a live scan), aggregates them under <workspace>/.agents/skills/<name> as symlinks to the canonical source (the source of truth stays in the project — never moved or copied), and then installs each for the runtimes by symlinking ~/.claude/skills/<name> and ~/.codex/skills/<name> at the aggregate. Runtime links point at the aggregate, not the source, so .agents/ is the single hub — re-pointing one aggregate link updates both runtimes' view. This deliberately replaces the legacy scattered layout (one runtime symlink straight into each project). Every link target is stored relative to the link's own dir (the workspace survives being moved/renamed), and paths are treated as opaque strings — spaces and : (Inbox:Outbox-style names) are first-class. --dry-run (the default) prints the plan and writes nothing; --apply executes; --json emits the structured plan/result. Sync is idempotent and self-healing: a re-run with the same set is a no-op, a moved source repoints, a removed skill is pruned across all three layers — and a non-symlink squatting a link path (a real dir like *-workspace) is refused, never clobbered (exit 1 if any link was refused). It maintains a markered ### Installed agent skills section (name + one-line description) in the workspace top-level README, idempotently (HTML-comment markers, replace-or-append; byte-identical output on an unchanged set).

The dashboard is a fuller read-only viewer (Projects + Automations) — localhost-only by default, opt-in servable on a tailnet. The Projects view is a lifecycle-scoped (Active default / All / Dormant / Archived, with counts) master-detail task tree — dotted-ID subtask disclosure, status dots, quadrant/recur badges, a right detail pane with the record markdown and copyable CLI commands. The Automations view (URL-persisted ?view=automations) joins three read-only sources over the live tree — declared machines (each project's _project/automations/<name>/automation.toml machines = [...]), the synced per-machine registries at .openworkspace/machines/<id>.toml (which machines actually activate a given automation), and that machine's last-run outcome + heartbeat age — into a machine-registry freshness strip plus per-automation cards showing schedule, project + lifecycle, where it's activated vs declared, per-machine state/last-run/staleness pills, and placement drift (declared-not-activated and activated-undeclared, computed the way doctor does) inline and as a top banner with a URL-persisted "Drift only" filter. It is served by a new GET /api/automations endpoint with the same security/cache posture as /api/scan (Host-header DNS-rebinding defense, GET/HEAD-only, the generic stale-while-revalidate ScanCache<T>). With no flags it binds 127.0.0.1 and accepts only localhost/127.0.0.1 in the Host header (the DNS-rebinding defense) — unchanged. To serve it over Tailscale, the operator sets a bind host (--host, or config host, default 127.0.0.1 — e.g. a Tailscale IP like 100.120.153.52 or 0.0.0.0) and an allowed-hosts set (--allow-host repeatable, or config allowed_hosts array) that is added to the secure default — e.g. --allow-host matteos-mac-mini.tailbd8a21.ts.net (a :port suffix is fine). The Host check still rejects anything outside the (default ∪ configured) set; it never opens up wholesale. Both dashboard dev and dashboard open take these flags and the same keys via --config. The dashboard remains strictly read-only (GET/HEAD; 405 otherwise) — exposing it on a tailnet exposes a viewer, not a mutation surface.

Serving a large workspace stays snappy via a short-TTL in-memory scan cache. A naive /api/scan re-walks the whole tree every request (~seconds on a real ~/Documents with deep Archives/Library and ~20 nested git repos). Two mitigations: (1) discovery skips foreign git working trees — a directory carrying .git that is not an OpenWorkspace project (a cloned repo, a code checkout) holds no projects and is no longer descended; this alone is a measured ~3.5x discovery speedup on the live tree (2.6s → 0.74s) while still finding every project, including nested ones and 2-levels-under-a-shelf ones like Dormant Projects/Life Admin/Personal Finance. (2) An in-memory scan cache (--cache-ttl MS, or config cache_ttl_ms, default 0 = off): the first request builds the scan, caches it in process, and serves it for the TTL window; once stale, a request serves the still-recent cached scan immediately and rebuilds in the background (stale-while-revalidate — no request blocks on the slow walk once warm). It is a rebuildable in-process cache, never a state file (nothing on disk; the live tree stays the source of truth), and the scan's generatedAt always reflects when the scan was actually built, never request time, so freshness shown to the user stays honest. TTL 0 is the default for the foreground dashboard dev case (always fresh); a long-lived served instance (the Mini) sets a small positive TTL (~15000ms suggested).

Tests

npm test             # build + node:test over dist/tests/**/*.test.js

All tests run against temp dirs under os.tmpdir() — never the live workspace, never the real ~/Library (the machine store is injected via OPENWORKSPACE_STORE_DIR). Frontmatter-codec fidelity is tested byte-for-byte against real legacy records committed under tests/fixtures/ (do not edit those files). tests/acceptance.test.ts holds the PRD acceptance checks: no state files ever written, duplicate-UID detection, state-named-subdir flagging, exact init skeleton, and worktree-post-lands-canonical.

Status (honest)

Implemented and green (314 tests): the foundation libs (lossless frontmatter codec, atomic fs, TOML, workspace discovery, machine store, UID-anchored canonical resolution, locked ID minting with rename-based stale-lock stealing), tasks (incl. reminders-as-tasks, recurrence, and machine-suffixed minting via home mint-suffix), decisions, forum (threads/messages/presence/inbox/who + the forum sweep retention verb), init (workspace + full project skeleton with the Appendix A orientation README, plus the .openworkspace/machines/<id>.toml registry heartbeat — now also carrying activations + last-run outcomes), doctor (the PRD §10 set minus the one item below — including conflict scan under .git/, state-named subdirs under any primitive, automation-manifest validation findings incl. the bare-secret hard error, [signature] path checks, declared-vs-activated placement drift + orphaned activations read from the synced registries, resolved-thread archive proposals, unanswered-question aging, doc-currency over the stamped READMEs and the shipped skill, git-posture reconciliation, machine-heartbeat staleness, stale-worktree-registration proposals), skills sync (cross-project skill aggregation into <ws>/.agents/skills/ + idempotent self-healing install for the Claude Code and Codex runtimes + a markered README section — see below), the read-only dashboard (Projects + Automations views, incl. --config, the /api/automations endpoint, and an opt-in tailnet-serving mode — see below), and the full projects CLI. Every workspace-routed command registers the canonical checkout with the machine store, and worktree roots are never registered nor resolved as canonical (the §6.3 split-brain guard, regression-tested against real git worktree add checkouts).

Automations are built (PRD §7; src/primitives/automations.ts + src/runner.ts; tests in tests/automations.test.ts + tests/runner.test.ts): automation.toml parse/validate (declared machines — matched against projects home machine-id — cron/calendar_interval + miss_policy, command arrays, static non-secret [run] env (env = { KEY = "value" }; plain strings only — non-string values, pointer-shaped values, and keys colliding with [secrets] are validation errors; under direct_exec it is a WARNING, since direct-exec plists carry no EnvironmentVariables), pointer-only [secrets], [supervise], [signature], on_dormant_project); the cron→StartCalendarInterval compiler with the PRD §7.1-pinned union semantics (DOM and DOW both restricted ⇒ fires when EITHER matches; conformance-tested, so the v0.2 AND bug cannot recur); late-binding plists (runner + project UID + name — never a project path, never a secret); apply [<name>|--all] with declared-machines reconciliation (undeclared ⇒ error, --force overrides; idempotent convergence — unchanged = no-op, cadence change = regenerate + reload); deactivate · list [--all] (every machine's synced registry with explicit staleness) · status (activation records ↔ launchd ↔ tree: stale-install, uninstalled-draft, orphans, placement drift in both directions) · prune · logs · run-now (through the runner path). The runner resolves UID→canonical at fire time, resolves [secrets] per run through the configured scheme resolvers (env-only — tests grep every written artifact for the resolved value), builds the child env as base env < [run] env (static) < resolved secrets and logs the static env key NAMES (# env: ... (static), values never logged, matching the # secrets: line), writes machine-partitioned logs (logs/<machine>/<stamp>.log, LOG_RETENTION newest kept), and appends last-run outcomes to its own machine's registry file (P15). launchctl + the LaunchAgents dir sit behind an injectable LaunchdAdapter; the OPENWORKSPACE_LAUNCHD_DIR env override selects a file-backed fake, which tests use exclusively.

The decision-1 TCC posture is wired (PRD §7.4, resolved 2026-06-10; Personal OS decision-1): the node that plists invoke the runner with is a machine-local factprojects home runner-node <path> stores it in the App Support store (validated: exists, regular file, executable; v1 posture = a dedicated copy of the official nodejs.org pkg build at a fixed path outside the tree, granted once per machine — see Tools/Automations/MINI_BOOTSTRAP.md "OpenWorkspace runner setup"). apply/list/status all resolve the node through one chain (injected → configured runner-node → process.execPath), so a configured-but-not-re-applied install correctly reports stale-install; an unset runner-node falls back to the current node with a WARNING in apply output (ApplySummary.warnings). projects home doctor carries the runner-posture probes (machine-local, best-effort, system binaries behind an injectable ExecFn seam): runner-node-unset (activations here but nothing configured → warn), runner-node-provenance (Homebrew Cellar path, or codesign -dv --verbose=2 failing / ad-hoc / no Developer-ID Authority → warn — the grant breaks on update), and claude-grant-staleness (claude's Documents-folder grant is path-keyed per version; the probe resolves the current version from the ~/.local/bin/claude symlink target and queries the user TCC db via sqlite3 for a matching path-keyed allow row — no match → warn "re-seed needed"; unreadable db → an info-level "unverifiable" finding, never an error).

Importers are built (PRD §11 step 4; src/importers.ts; tests in tests/importers.test.ts, run against the real legacy fixture corpus): projects import legacy is dry-run-first (--apply executes exactly the plan; per-record audit lines in both modes; --all iterates every project with per-project failure isolation — a failing project is refused whole, with an honest refusal line, while the others still apply and print their audits). State fidelity: legacy Backlog.md statuses map to the native vocabulary; parent_task_id normalizes into the dotted ID (disagreement is a plan error); bodies and unknown frontmatter keys are byte-preserved; archived/completed legacy records land in tasks/archive/. Reminders import as tasks (surface_onhidden_until; surfaced → live todo; dismissed → archived task keeping the closing-reasoning log line; promoted → archived with a cross-ref; new IDs minted above the legacy max under the project's mint lock). Dirchannels flatten into forum threads (<date>--<channel>--<slug>; one immutable maildir file per messages.jsonl line; sqlite/token/pty tool state listed as skips). Idempotency is verified, not assumed: an ID hit must be the same import (same ID-carrying filename or byte-equal content) — a legacy ID colliding with a pre-existing native task is a loud plan error, never a silent "already imported"; intra-plan duplicate targets (e.g. a duplicated JSONL line after an iCloud append glitch) are deduped when byte-identical and errored when content differs, so an error-free plan can never crash mid-apply. Audit completeness: drafts/, archive/-beyond-tasks/, and the legacy v0.2 reviews/ and proposals/ dirs (under the project's control-plane directory) all get honest per-file skip lines.

Lifecycle metadata-as-truth + the reconciler are built (decision-2 draft; src/reconcile.ts, the lifecycle/intent helpers in src/lib/workspace.ts + src/lib/machine.ts; tests in tests/reconcile.test.ts, tests/lifecycle-metadata.test.ts, tests/lifecycle-intent.test.ts). The capability is whole: readDeclaredLifecycle/writeDeclaredLifecycle (lossless, sheds the key on active), effectiveLifecycle (declared-wins, location-fallback), the machine-local intent-log, classifyDrift (git → intent-log → propose), reconcilePlan/applyReconcile (dry-run default; --apply; --auto for the glitch-certain class; same-ID dedup + ghost-dir healing), the metadata-primary projects lifecycle and the projects reconcile CLI (incl. --revert/--adopt-location human tiebreaks), and doctor's read-only drift report. The lifecycle vocabulary is exactly active|dormant|archived (Matteo dropped ongoing 2026-06-11; the legacy ongoing boolean is removed). Ratify-gated (not yet flipped on the live tree): the principle amendment into the architecture docs, the schema 2→3 bump, wiring the Mini's pre-automation reconcile --apply --auto step, and record-retention field-as-truth (Track A's retired: — deferred per Principle 6, the observed corruption was lifecycle, not retention). See Personal OS/_project/decisions/decision-2 - lifecycle-metadata-as-truth.md. No ~/Documents/.git monolith is part of this design — git is per-project and optional.

Not yet implemented (each stubbed loudly, never silently no-oped):

  • The §7.4 TCC question is RESOLVED (decision-1, 2026-06-10) and the code deltas are in (runner-node + the doctor posture probes, above) — what remains is per-machine bootstrap, not code: installing the official-pkg node copy at the fixed path and seeding the grants is a supervised one-time act per machine (steps in Tools/Automations/MINI_BOOTSTRAP.md). The runner's execute() seam still spawns (measured sufficient: leaves ride their own position-independent folder grants); direct_exec = true remains the documented hybrid fallback for claude jobs (ProgramArguments = the command itself, WorkingDirectory baked at apply time; [secrets] rejected in that mode — no runner to resolve them) with decision-1's switch trigger: grant-staleness probe firing more than ~monthly.
  • miss_policy runtime semantics — validated and recorded as manifest metadata for the supervise pass (which lives in C3 per the PRD); the runner behaves as skip. Doctor warns on any non-skip value so the key is never a silent no-op. [schedule] timezone is rejected at validation until a consumer exists (launchd fires machine-local; silently-local scheduling under a declared timezone would be the worst of both).
  • The §11.4 manual migration items — the two v0.2 review records → tasks and the finance _proposals/ re-home are migrating-agent work, not importer scope; the importer lists those legacy dirs as audited skips so the checklist cannot miss them.
  • One doctor gap: the aging-untracked-forum-message commit-sweep proposal (the §6.3 commit-=-retention nudge; needs git-tracking introspection of the canonical forum).

Pre-migration checklist (real-corpus blockers, found by running the importer against all six preserved legacy-imports/ snapshots). Loud whole-project refusal is the designed posture; these two must be hand-fixed before §11 step 4 can apply cleanly:

  1. Personal OS_tasks/tasks/task-181 - Design-agent-purchase-delegation-mechanism.md has an unquoted : in its title: value (invalid YAML written by the legacy tool); quote the title.
  2. Home Automationtask-1 - Approve-Samsung-TV…pairing-prompt.md and its iCloud duplicate … 2.md both carry id: TASK-1; delete the duplicate (the contents diverge — duplicate legacy task id refuses the project until resolved).

Also note for the re-migration pass: the PRD §6.1 example .gitignore stanza shows an unanchored archive/ pattern, which contradicts §4.8/§11.4 (it would git-ignore tasks/archive/ and forum/threads/archive/, the committed retention homes). The stamp ships anchored (/archive/), and doctor proposes anchoring wherever it finds the unanchored legacy pattern; the PRD example should be corrected to match when it is next revised.

The v0.2 implementation under Personal OS/Tools/OpenWorkspace/ is reference-only (21 known defects) and is retired after re-migration.

About

OpenWorkspace — a filesystem-first personal-workspace substrate (projects CLI)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors