Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 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/, then build:client (vite build in src/dashboard/client),
                      # then copies the client's single-file dist/index.html to dist/src/dashboard/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. The dashboard client (src/dashboard/client/) is its own npm package with its own node_modules (React, Vite, vite-plugin-singlefile) — npm install at the root does not reach it; npm run build does, via build:client.

Resuming Work

Start with projects task list --json in this repo, then read the relevant design note in _project/wiki/ for the current arc. For automation-runtime work, read _project/wiki/automation-runtime-v2-design.md, then run npm test and projects doctor. For Mini rollout/remediation, check task-3/task-4 first and do not deploy into the separate Mini checkout until its dirty worktree has been reconciled.

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' runs_on = [...] (or legacy 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 · supervise
                    supervisor apply|status|deactivate · 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 client is React + TypeScript, built by Vite to a single self-contained HTML file (src/dashboard/client/; decision-8 in Personal OS). It is its own npm package — local node_modules, its own tsconfig (excluded from the root server build so browser-targeted .tsx never leaks into the Node tsc compile) — built via vite-plugin-singlefile so the shipped artifact stays exactly what it was before the migration: one dependency-free, CSP-safe HTML file with no external chunks and no CDN, now served from dist/src/dashboard/index.html. It is a faithful port of the previous vanilla-JS shell: header (view tabs, attention chips, refresh), left rail (lifecycle scope, search, status filter, subtasks toggle), the master project/task tree (collapsible cards, dotted-ID subtask disclosure, status dots, rollups, badges), the detail pane (metadata, rendered markdown body, status/done/note Actions wired to the decision-1 write path with optimistic patch+revert, copyable CLI commands), and the Automations view (machine heartbeat strip, per-automation machine rows, drift banner/filter) — same URL-persisted state (?view=&scope=&status=&q=&subtasks=&task=&project=&autofilter=). The old src/dashboard/index.html stays in the tree as a reference fallback but is no longer what's served. The rail/list/detail columns are now resizable (drag either divider; widths clamp to sane min/max and persist in localStorage, collapsing back to a single stacked column below the existing ~980px mobile breakpoint), the master task tree supports keyboard navigation (roving tabindex over the visible li.task rows — ArrowUp/ArrowDown move focus and selection in rendered DOM order, Enter re-confirms), and the whole client follows dark mode via prefers-color-scheme (no explicit toggle — palette is CSS custom properties, swapped in one @media block).

POST /api/project/reveal opens a project's root in Finder, or its Obsidian vault if one exists, from the client (a "Reveal" control on each project card) — the browser can't spawn a process itself, so this is a narrow, loopback-gated endpoint (same write-gate as the task mutation endpoints, since spawning a process is at least as sensitive as a file write) that resolves the client-supplied project uid to an absolute root server-side and shells out to macOS open, never to a client-supplied path. {target: "obsidian"} 422s when the project root has no .obsidian directory (surfaced in the scan as hasObsidianVault so the client only shows the control when it'll work); any platform other than macOS 501s without spawning anything. The process launcher is injectable (DashboardOptions.processOpener) so tests never actually invoke open.

GET /events (SSE) replaces manual-Refresh-only polling with live push. The warm read-model (below) now emits a change event on every write-through mutation and every fs.watch reconciliation (never on the periodic self-heal rebuild, which would otherwise spam every subscriber on every server-restart-adjacent rebuild); /events is read-gated exactly like /api/scan (Host-header check, GET/HEAD-only posture), bounds its connection count, sends periodic keepalive comments, and cleans up listeners on disconnect (plus a close() override so server shutdown doesn't hang on a live stream). The client opens one EventSource, debounces a burst of events into a single refresh(), and reconnects with exponential backoff on error — falling back to the pre-existing manual Refresh button if EventSource is unavailable or stays down.

The task detail pane has a narrow, library-mediated body editor and interactive Acceptance-Criteria checkboxes (decision-9 in Personal OS, deliberately not a general Markdown/WYSIWYG editor — see the decision for why that fence matters). An "Edit" toggle swaps the rendered body for a plain <textarea> (no editor library) seeded with the task's full current body, so ## Final Summary/## Log are visible and a save never silently drops them; Save routes through a new setTaskBody library primitive that preserves frontmatter verbatim and rewrites only the body. Each rendered - [ ]/- [x] line also gets a real native checkbox (event delegation over the rendered HTML, no library) that calls a new toggleChecklistItem primitive to flip just that one line. Both entry points are hash-guarded the same way task mutations already were: the call carries expectedHash (the warm model's per-file sha256), and a hash mismatch throws ConflictError — mapped to an HTTP 409 (POST /api/task/body, POST /api/task/checkbox) — rather than clobbering a concurrent CLI or Obsidian edit; the client reverts its optimistic patch and asks the user to reload.

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 GET /api/automations with the same security posture as /api/scan (Host-header DNS-rebinding defense, GET/HEAD-only) over the generic stale-while-revalidate ScanCache<T> (automations are not yet on the warm model below). 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's read path stays GET/HEAD-only (405 otherwise); writes are the decision-1 loopback-gated exception, not a general mutation surface.

GET /api/automation/logs?project=&name=[&machine=&run=] serves the machine-partitioned per-run logs the runner writes under _project/automations/<name>/logs/<machine>/<stamp>.log — the same logsFor() the CLI's automation logs uses. It returns the run list ({machine, stamp}, newest last, across machines) plus the selected (or latest) run's content, tail-capped at 128KB so a large claude --print stdout can't blow up the response, and traversal-safe by construction (a run is only served if it matches a file logsFor() itself enumerated). It's read-gated like /api/scan (no write surface). In the Automations view, each automation card has a "logs" disclosure with a run picker and a scrollable log view — turning "is it healthy?" into "what did this run actually do, and why did it fail?". Secret values never appear (the runner logs secret keys only).

/api/scan and /api/task are served from a warm, incrementally-maintained read model, not a cache with a TTL (decision-7 Phase 1b, Personal OS). A naive full-tree walk on every request costs ~seconds on a real ~/Documents (deep Archives/Library, ~20 nested git repos) — discovery already skips foreign git working trees (a directory carrying .git that is not an OpenWorkspace project holds no projects and is no longer descended; a measured ~3.5x discovery speedup, 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), but the dashboard now goes further: the WarmModel builds the scan once at server start and then keeps it current by construction rather than by expiring it — write-through on the dashboard's own mutation path (a status/done/note edit re-reads only the one file it just touched, no invalidation of the rest) and fs.watch on each project's tasks/ directory (reconciling only the touched file(s) by content sha256, debounced, with self-echo suppression so the watcher doesn't re-process its own write-through update, plus a periodic full-rebuild fallback in case a watcher fails to start on some path). Reads are always served straight from memory — no request ever blocks on a tree walk once the server is up. Nothing is persisted to disk (SQLite stays deliberately deferred, per decision-7's "earn the heavy parts"); the model is a rebuildable, disposable derived index, never a state file — the live tree stays the sole source of truth, and a dropped/restarted model reconstructs itself identically from the files and the watcher. /api/automations is untouched by this — it still runs on the original stale-while-revalidate ScanCache<T> described below, which remains configurable via --cache-ttl MS / config cache_ttl_ms.

The write path closes a data-loss race: optimistic concurrency in touchAndWrite (decision-7 Phase 1a, Personal OS). Two writers — the CLI/dashboard and a human editing the same file in Obsidian — could previously race: both read, one writes, the other's write silently clobbers the first with stale content. Every task write now captures a sha256 of the exact bytes it read, then re-reads and re-hashes the file immediately before the atomic write; a mismatch (or the file having vanished underneath it) throws ConflictError instead of clobbering, turning a silent data-loss race into a visible, retryable conflict. writeFileAtomic was already temp-file + fsync + rename, so the closed hole was purely the read-modify-write gap, not the write itself.

Serving /api/automations stays snappy via a short-TTL in-memory scan cache (unchanged from before Phase 1b, and not yet migrated to the warm model above). 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 (451 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 dashboard (Projects + Automations views, incl. --config, the /api/automations endpoint, an opt-in tailnet-serving mode, resizable panes + keyboard nav + system dark mode, the /api/project/reveal Finder/Obsidian endpoint, /events SSE live-updates, the decision-9 narrow body-edit + Acceptance-Criteria-checkbox endpoints, and the /api/automation/logs per-run log viewer — 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/primitives/automation-runs.ts, src/primitives/automation-scheduler.ts, src/primitives/automation-supervisor.ts, src/runner.ts; tests in tests/automations.test.ts, tests/runner.test.ts, and the focused runtime tests): automation.toml parse/validate (declared placement via runs_on — the forward name for the executor set, matched against projects home machine-id; the legacy machines key is still accepted as an alias, both parsing to the same AutomationManifest.machines field, and declaring both is only accepted when they agree — runs-on-machines-conflict otherwise — cron/calendar_interval + miss_policy, provider-neutral [run].kind, overlap controls, 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 runner-only semantics are warnings), pointer-only [secrets], [supervise], [signature], on_dormant_project — now read against the project's effective lifecycle, decision-2's metadata-primary value with location as fallback only, closing the one remaining spot where folder location was still the primary lifecycle signal); 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 placement drift only) · supervise (one local recovery tick) · supervisor apply|status|deactivate (one com.openworkspace.supervisor LaunchAgent) · prune · logs · run-now (through the runner path). The runner resolves UID→canonical at fire time, creates a machine-local run ledger attempt before synced-tree reads, records phase/outcome in ~/Library/Application Support/OpenWorkspace/automation-runs/, resolves [secrets] per run through 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, writes machine-partitioned logs (logs/<machine>/<stamp>.log, LOG_RETENTION newest kept), and mirrors last-run outcomes to its own synced machine 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.
  • Advanced missed-run schedulingskip/catch-up decision logic exists in the Runtime v2 scheduler helper and manifests validate misfire_grace_seconds/max_catch_up; the current per-automation launchd fire path still runs a single managed occurrence at fire time. fail-loud and coalesce are reserved and doctor warns when they appear. [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).
  • The peer-coordination machinery (heartbeats, staleness warnings, cross-machine placement-drift healing, mint-suffix IDs) is cataloged, not removed. It was built for a multi-machine, independently-applying model and every laptop automation still depends on parts of it today. _project/wiki/compute-plane-vestigial-catalog.md maps exactly what becomes safe to cut once a single always-on hub is the sole executor (decision-7 Phase 4, gated on token rotation — Personal OS task-201/203) — a hub is not a peer, so questions phrased as cross-machine reconciliation collapse to "is the hub itself healthy." Nothing in that catalog should be removed opportunistically; removal is a hub-phase decision.

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

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages