mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
f9bc0bf05dd1997eb80f6febe053abdec4ecb573
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8c0fb8dc01 |
feat(tools-pr): add maintainer PR-duty workspace (#1259)
* feat(tools-pr): add maintainer PR-duty workspace
Adds `tools/pr` as the maintainer-only control plane for PR-duty work on
this repo. Thin `gh` wrapper that encodes repo-specific knowledge:
review lanes, forbidden surfaces, lane-specific checklists, validation
command derivation from touched packages.
Subcommands:
- `list` — triage open queue by lane and review-state bucket.
- `view <num>` — agent-friendly review brief for a single PR.
- `classify [num]` — emit script-level tags for one PR or the whole
open queue; full-queue JSON output lands under `.tmp/tools-pr/classify/`
with rate-limit telemetry per run.
- `assignment` — assigner-perspective view of PR ownership, idle time,
and blockers (derived from existing tags; no new judgments).
Tag dictionary (13 tags) covers: bot-only-approval, needs-rebase,
forbidden-surface, unlabeled, duplicate-title, non-ascii-slug,
maintainer-edits-disabled, org-member, unresolved-changes-requested,
stale-approval, and three awaiting-* timing tags. Each rule is
expressible as one factual sentence over `gh` data + repo paths — see
`tools/pr/AGENTS.md` for the full dictionary plus precision rules.
Templates in `tools/pr/templates/*.md` are aesthetic references for
recurring maintainer comments (duplicate-title ask, awaiting-author
nudge, agent-review brief shape). `templates/examples/` holds
frozen-in-time agent-review snapshots for three PR shapes.
Infrastructure:
- `gh()` wraps `execFile` with minimum-touch retry (2 attempts at 1s + 2s
backoff) on transient 5xx / network errors. Persistent failures still
surface — retry is anti-jitter, not an exponential-backoff resilience
layer.
- Heavy chunks (`reviews`, `comments`, `commits`, assignment timelines)
use cursor-paginated `gh api graphql` via `fetchPaginatedPrList` to
stay under GitHub's GraphQL server-side timeout. Light chunks stay on
`gh pr list --json`.
- `fetchOrgMembers` cached per process via `gh api orgs/<owner>/members
--paginate`.
Wiring:
- Root `package.json` adds `pnpm tools-pr` to the allowed root entry
points.
- `scripts/postinstall.mjs` builds `tools/pr` alongside other workspace
packages.
- `scripts/guard.ts` allowlists `tools/pr/bin/tools-pr.mjs` and
`tools/pr/esbuild.config.mjs`, and adds `pr/` to the `tools/` top-level
layout allowlist.
- Root `AGENTS.md` and `tools/AGENTS.md` document the new command
surface, root-command-boundary update, and per-tool ownership.
* docs(agents): brief tools-pr in root AGENTS.md, link to tools/pr/AGENTS.md
Adds a `PR-duty tooling` section to the root AGENTS.md summarising what
`pnpm tools-pr` is, listing the four common subcommands (list / view /
classify / assignment), and pointing readers to `tools/pr/AGENTS.md` for
the full tag dictionary, operational playbook, templates, and design
rules. The section keeps root-level guidance to high-level orientation
while details stay local to the tool's own AGENTS.md.
* fix(tools-pr): drop overly broad touches-root-package.json forbidden hit
`deriveForbidden` was flagging any change to root `package.json` as a
forbidden-surface hit, but AGENTS.md §Root command boundary only forbids
specific *lifecycle* aliases (pnpm dev / test / build / daemon / preview
/ start) — tools-control-plane entrypoints like `pnpm tools-pr` are
explicitly allowed. Distinguishing "forbidden alias" from "allowed
entry" requires reading the diff content, which is `pnpm guard`'s job
rather than a path-derived classify tag.
Dogfooded on this branch's own PR (#1259), which added the `pnpm
tools-pr` script and was incorrectly flagged. Removing the hit aligns
the `forbidden-surface` tag with what tools-pr can mechanically detect
from file paths alone (apps/nextjs/, packages/shared/).
* fix(tools-pr): paginate commits fetch, recognise ready-to-merge, escape title-index separator
Three review follow-ups on #1259, all factual fixes:
- `fetchOpenPrCommits` now uses `fetchPaginatedPrList` instead of a
one-shot `pullRequests(first: $first)` query. GitHub GraphQL caps
connection page size at 100, so the previous implementation would
fail at runtime when callers passed `--limit > 100`. The paginated
path makes the commits fetch consistent with the other heavy chunks
(reviews, comments, assignment timelines) and removes the artificial
ceiling entirely. The `limit` parameter is dropped from
`fetchOpenPrCommits`; the CLI `--limit` continues to bound the
`gh pr list --json` chunks.
- `deriveStatus` in `assignment.ts` now reads `facts.reviewDecision`
and `facts.mergeStateStatus`. When the PR is `APPROVED` with merge
state `CLEAN` or `UNSTABLE` and carries no blockers, status renders
as `ready to merge` instead of falling through to `in review`. The
assignment view loses its main triage signal without this — a clean
human-approved PR rendered identical to a REVIEW_REQUIRED one.
- `tags.ts:tagDuplicateTitle` and `tags.ts:buildContext` both
constructed the title-index key with a literal NUL byte between
author and title, which made the file appear as binary in `git diff`
/ review tooling. Replaced the literal byte with a Unicode escape
sequence in source; the runtime string value is identical, the
source stays plain text and round-trips through review tooling
cleanly.
* fix(tools-pr): raise default --limit to 1000 to cover the live open queue
mrcfps flagged that `tools-pr list` (and `classify --all`, `assignment`)
defaults to `--limit 100`, which silently drops every PR past the first
100 in the open queue. The repo currently sits at 104 open PRs, so the
out-of-the-box run was already omitting four PRs.
Raise the default to 1000 in `list.ts`, `classify.ts`, and `assignment.ts`,
and remove the now-pointless 200 ceiling — `gh pr list --limit N` paginates
internally, so a high cap is cheap. Users can still pass `--limit <small>`
for a truncated preview. CLI help text on the three subcommands updated to
match.
* fix(web): pass designTemplates to ProjectView render helper
#955 made `designTemplates` a required Prop on ProjectView, but the test
helper added in #1244 (`renderProjectView` in
`ProjectView.api-empty-response.test.tsx`) was never updated. The two
PRs landed on main without conflicting, leaving `apps/web` typecheck red
for every PR that rebases past
|
||
|
|
b5eb8c1647 |
feat: generic skills + split skills/design-templates + finalize-design API (#955)
* feat: general-purpose skills with @-mention composition and user import
Lift skills from "one mode-bound skill per project" to a generic capability
the user can compose per turn:
- Daemon: scan multiple skill roots (user-skills under runtime data, then
the bundled `skills/`); user-imported skills can shadow built-ins by id.
- New `POST /api/skills/import` and `DELETE /api/skills/:id` endpoints,
with CONFLICT/BAD_REQUEST/NOT_FOUND error codes and built-in delete
protection.
- ChatRequest gains `skillIds: string[]`; the chat run concatenates each
picked skill's body (and merges craftRequires) into the system prompt
for that turn only — the project's persistent `skillId` is untouched.
- Web composer: `@` popover now lists skills alongside project files;
picks render as removable chips above the textarea and ride along with
the request as `skillIds`.
- Settings → Library: import form (name/description/triggers/body),
per-card delete for user skills, "user" origin badge.
* chore(web): drop welcome pet teaser + add ds→prompt-template mapping util
- SettingsDialog: remove the inline pet adoption teaser from the welcome
panel so the first-run modal stays focused on configuration.
- New `inferPromptTemplateCategoriesForDs(ds)` helper that maps a design
system's authored metadata to prompt-template gallery categories.
Imported by the design-system gallery wiring on a sibling branch; no
callers in this branch yet.
* feat: split skills/design-templates and add finalize-design API
Phase 0 of the skills/design-templates refactor (specs/current/
skills-and-design-templates.md):
- Move ~104 rendering catalogue entries from skills/ to design-templates/
and keep skills/ for the small set of functional skills that *do work*
on user input (utilities, briefs, packagers).
- Add design-templates/AGENTS.md and skills/AGENTS.md describing the
contract, and a brand-agnostic craft/ surface for opt-in craft rules.
- Daemon: add DESIGN_TEMPLATES_DIR / USER_DESIGN_TEMPLATES_DIR roots and
an /api/design-templates surface mirroring /api/skills. Asset/example
routes still span both registries so existing srcdoc URLs keep
resolving across the rename.
- Web: split LibrarySection into SkillsSection + DesignSystemsSection,
rename the EntryView "Examples" tab to "Templates", and update locales
+ the New-project picker accordingly.
Adds the finalize-design endpoint:
- New apps/daemon/src/finalize-design.ts and packages/contracts/src/api/
finalize.ts — one-shot synthesis of a project's transcript + active
design system + current artifact into <projectDir>/DESIGN.md via the
Anthropic Messages API. Per-project .finalize.lock mirrors the
transcript-export hygiene from PR #493; provider credentials are not
persisted by the daemon.
Other supporting changes:
- README + AGENTS.md updates to document the new directory split and
craft/ surface, plus i18n strings across 13 locales.
- Test refactors and new coverage (finalize-design, runs, sidecar
server, plus refreshed daemon integration tests).
- .gitignore: scope the *.exe ignore to /OpenDesign.exe so legitimate
vendor binaries are no longer hidden.
* fix(merge): move clinical-case-report to design-templates/
Origin/main added the clinical-case-report skill under skills/ before
the skills/design-templates split landed. Its od.mode is prototype, so
per specs/current/skills-and-design-templates.md it is a design template
and belongs alongside the other rendering catalogue entries — not under
the slimmed-down functional skills/ root. Moving it keeps the EntryView
Templates tab consistent with origin/main's intent.
* feat(skills): curated design/creative catalogue + collapsible Settings rows
Seed ~100 curated design/creative skill stubs under skills/ sourced from
awesome-claude-skills (ComposioHQ) and awesome-agent-skills (VoltAgent).
Each stub carries an od.category tag so the new filter pill row in
Settings -> Skills can group them. The seed script
(scripts/seed-curated-design-skills.ts, pnpm seed:curated-design-skills)
is idempotent: it only creates folders that don't already exist, so
hand-edited stubs are never overwritten.
- Daemon: parse and surface od.category on SkillInfo with a strict slug
normaliser; mirror the field on SkillSummary in @open-design/contracts.
Category is purely a UI hint — system-prompt composition is unchanged.
- Web: rewrite SkillsSection from a left-list / right-detail grid into a
vertical stack of collapsible rows mirroring the External MCP panel
(header always visible with name + mode/source/category pills + per-row
enable toggle; SKILL.md preview, file tree and inline edit form expand
on demand). Add a Category filter row above the list. Reorder Settings
nav so Skills + External MCP sit above the Composio/MCP cluster. Update
composer placeholder/hint across 17 locales to advertise '@ files or
skills · / for commands'.
- Docs: extend skills/AGENTS.md with the curated catalogue rules
(idempotency, category vocabulary, no upstream vendoring).
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(skills): teach localized-content + system-prompt tests about the skills/design-templates split
mrcfps blocking review on PR #955: the skills/design-templates split
(
|
||
|
|
f2db5a749c |
chore: enforce PR→issue linking discipline (#1263)
PRs that omit Fixes #N break the release-time reverse lookup (issue → closing PR → merge sha → first containing tag), since the auto-link only fires on the explicit closing keywords. We've been doing this by hand on recent fixes; codify it so future PRs don't drift. - Add .github/pull_request_template.md with a Fixes # placeholder so the link surface is in front of the author by default. - Add a corresponding bullet to the Bug follow-up workflow in the root AGENTS.md so the discipline lives next to the methodology that produces issue-linked work. |
||
|
|
a797e079b1 |
fix(desktop): exit fullscreen before hiding window on macOS close (#1249)
* fix(desktop): exit fullscreen before hiding window on macOS close (#1215) When a preview is in 演示 → 全屏 mode, the macOS close handler called window.hide() directly, leaving the OS fullscreen Space orphaned as a black screen — the window vanished but the Space stayed up. Extract hideWindowExitingFullscreen as the named invariant ("hide, but first leave fullscreen so the OS Space tears down with the window") and route the darwin close handler through it. The hide is deferred until 'leave-full-screen' fires so we don't race the OS Space teardown. Bootstraps Vitest on apps/desktop with a single test under tests/main/hide-window-exiting-fullscreen.test.ts that exercises the helper through a structural mock — the bug shape is pure logic, no real Electron window required. Spec was red against a hide-only helper and green after the leave-full-screen sequencing. * docs(agents): codify bug follow-up workflow Distill the spec-first / cheapest-layer / scope-discipline / invariant-shaped-fix / baseline-diff playbook used recently on #135 and #1215 into a top-level subsection of root AGENTS.md, framed as a default action shape with explicit room for case-by-case judgment rather than a hard rule. Includes a single pointer back to the worked example spec. * docs(agents): require staged human verification for visible bugs Add the human-verification gate as a sixth bullet in the Bug follow-up workflow. UI / platform-native / animation symptoms can pass green specs and still ship the visible regression — proven by #1215, where the desktop unit test green-lighted the helper logic but only a side-by-side buggy-vs-fix run on a real macOS Space proved the black-screen actually went away. Reinforces the production-API-only seed constraint while we're there: source-level backdoors prove a fake flow, not the real one, so they invalidate the verification. * fix(desktop): defer hide across the fullscreen-enter transition (#1215) mrcfps observed on PR #1249 that the close handler only catches windows already in fullscreen — Electron's enter-full-screen event is async on macOS, so isFullScreen() can still read false during the OS Space transition triggered by requestFullscreen(). A close in that window took the plain hide() path and stranded the same black Space the fix was meant to eliminate. Track in-flight fullscreen entry from webContents.enter-html-full-screen (set) and BrowserWindow.{enter,leave}-full-screen (clear), and surface it through WindowFullscreenSurface.isEnteringFullscreen. The helper now parks on enter-full-screen until the OS confirms the Space, then runs the existing exit-then-hide path. Adds a regression test ("waits out a fullscreen-enter transition before exiting and hiding") that goes red against the previous helper. |
||
|
|
bdd49ebc47 |
docs: add repository-wide code review guidelines (#927)
* docs: add repository-wide code review guidelines Introduces docs/code-review-guidelines.md as the operational review standard layered on top of AGENTS.md, and adds a Code review guide section to AGENTS.md that points reviewers at it. The guide codifies the Product relevance test as a pre-implementation gate, names the canonical list of forbidden surfaces, lists the ownership areas in scope, and defines five review lanes: default code/tests, contract and protocol changes, design-system additions, skill additions, and craft additions. It also captures the secrets, runtime data, performance, and maintainability checks that previously lived only as oral conventions, and aligns the approval bar with the validation rules in AGENTS.md. AGENTS.md remains the source of truth when the two disagree; the new doc is the operational guide on top of it. * docs: tighten review guidelines for governance docs and bugfix discipline - Reference scripts/guard.ts as source of truth for guard checks - Add governance documentation as an explicit in-scope category - Require reviewers to build a module/caller map before commenting - Add bugfix-specific reproduction and regression-test checks - Carve out documentation-only exception in the approval bar * docs: align review guidelines with repository policy Keep the review scope aligned with maintained workspace surfaces and preserve AGENTS.md as the authoritative validation bar. Generated-By: looper 0.6.3 (runner=fixer, agent=opencode) |
||
|
|
f1cdb2844a |
test(e2e): gate beta packaged runtime (#637)
* test(e2e): gate beta mac packaged runtime * test(e2e): separate ui automation layout * test(e2e): move localized content coverage * chore(release): prepare packaged 0.4.1 beta validation * test(e2e): keep ui lane playwright-only * fix(web): keep chat recoverable after conversation load failure * fix(desktop): honor native mac quit |
||
|
|
bbdd4e84b5 |
chore: enforce test directory conventions (#496)
* chore: enforce test directory conventions Move package, app, and tool tests out of src and add guard enforcement so source directories stay source-only. * ci: use guard and package-scoped tests Run the new repository guard in CI and keep test execution aligned with package-scoped commands after removing root aliases. * ci: align stable release guard check Use the new repository guard in stable release verification after replacing the residual-JS-only script. * chore: tighten test layout enforcement Enforce sibling tests directories, typecheck moved test suites with dedicated configs, and refresh remaining guidance that pointed at src-based tests. * chore: clarify no-emit test tsconfigs Explicitly disable declaration-only emit in test tsconfigs so review tooling sees they are no-emit typecheck configs. |
||
|
|
71bf5634e8 |
feat(daemon): allow OD_MEDIA_CONFIG_DIR to relocate media-config.json (#411)
* feat(daemon): allow OD_MEDIA_CONFIG_DIR to relocate media-config.json `media-config.ts:configFile()` always wrote to `<projectRoot>/.od/`, which fails when the daemon runs from a read-only install (Nix store, immutable image, etc.) — `PUT /api/media/config` then 500s with ENOENT/EROFS on the mkdir. Add an opt-in `OD_MEDIA_CONFIG_DIR` env var that, when set, redirects the file to `<OD_MEDIA_CONFIG_DIR>/media-config.json`. Existing workspace-local installs are unaffected — the default still resolves to `<projectRoot>/.od/media-config.json`. The override is intended to be pointed at the same writable directory as `OD_DATA_DIR` (`~/.od`, `$XDG_CONFIG_HOME/open-design`, etc.) by the supervisor that launches the daemon. Adds a test pinning the override path so the fallback semantics don't silently regress. * fix(daemon): align OD_MEDIA_CONFIG_DIR with OD_DATA_DIR semantics + cover write path Address PR review: * The previous diff trimmed the env value but skipped path expansion, so `~/.od` would land at literal `./~/.od/media-config.json` and relatives anchored against process.cwd instead of <projectRoot>. Mirror server.ts:resolveDataDir() — `~/` expands to homedir, relatives resolve against projectRoot. Pinned with three new test cases (absolute, `~/`, relative-against-projectRoot). * The packaged daemon (apps/packaged/src/sidecars.ts) doesn't pass OD_MEDIA_CONFIG_DIR through to the child and its allowlist won't forward it, so packaged installs would still fail. Add a fallback precedence: OD_MEDIA_CONFIG_DIR > OD_DATA_DIR > <projectRoot>/.od. Packaged installs (which already set OD_DATA_DIR) and the Home Manager / NixOS modules now route media-config there for free, no separate plumbing required. Workspace-local installs are untouched. * Add a write-path test that points OD_MEDIA_CONFIG_DIR at a not-yet- existing nested directory, calls writeConfig, and asserts both file creation and round-trip read. This reproduces the original PUT /api/media/config failure mode. * Document the precedence chain in AGENTS.md (single source of truth for module-level concerns) and the storage row of README.md, plus expand the file's header comment with a migration note for users who already had a custom OD_DATA_DIR alongside a workspace media-config.json. |
||
|
|
02638af353 |
Add linux x64 AppImage to tools-pack and release workflows (#369)
* feat(tools-pack): extend config types for linux platform
* feat(tools-pack): add linux resource files (icon, .desktop template)
* feat(tools-pack): export linuxResources paths
* feat(tools-pack): scaffold linux.ts module
* chore(tools-pack): add vitest devdep for linux lane unit tests
* feat(tools-pack): add buildDockerArgs helper for containerized linux builds
* chore: update pnpm lockfile after adding vitest dep
* feat(tools-pack): add renderDesktopTemplate helper
* fix(tools-pack): use @@ICON_PATH@@ token in linux .desktop template
Reviewer flagged the third .replace() in renderDesktopTemplate as dead code
because the template hardcoded Icon=open-design-@@NAMESPACE@@ instead of
using @@ICON_PATH@@. Switch the template to @@ICON_PATH@@ so install logic
controls the icon stem name independent of namespace, and move the
sanitizeNamespace assertion out of the renderDesktopTemplate describe block
into its own describe.
* feat(tools-pack): add matchesAppImageProcess helper
* test(tools-pack): cover matchesAppImageProcess missing APPIMAGE env case
Closes a coverage gap flagged by code review: a process whose executable
matches /tmp/.mount_*/AppRun but has no APPIMAGE env should be rejected.
The implementation already returned false for this case (undefined ===
installPath is false); this test pins the behavior explicitly.
* feat(tools-pack): implement packLinux native build path
* fix(tools-pack): packLinux extra resources, output pre-clear, publish never
Code review flagged three plan-level omissions in packLinux that mac.ts
handles correctly:
1. writeAssembledApp now writes packagedConfigPath (open-design-config.json)
with namespace, nodeCommandRelative, and namespaceBaseRoot. Without it
apps/packaged falls back to defaults at runtime and cannot find the
namespace runtime tree.
2. writeLinuxBuilderConfig now bundles the resource tree and packaged
config into the AppImage via extraResources. Without it the running
app cannot find skills/, design-systems/, craft/, frames/, or the
bundled bin/node.
3. runElectronBuilderLinux now pre-clears appBuilderOutputRoot and passes
--publish never to electron-builder, preventing stale-artifact bleed
between runs and accidental publish attempts in CI when env tokens
are present.
Also aligns appId with mac/win (io.open-design.desktop) and drops a
no-op productNameSafe template-literal.
* feat(tools-pack): implement containerized linux build via Docker
* feat(tools-pack): register linux CLI commands
* fix(tools-pack): align linux electron-builder config with mac.ts
Smoke testing the AppImage revealed the daemon sidecar was missing from
the bundled app.asar:
Cannot find module '@open-design/daemon/dist/sidecar/index.js'
Root cause: writeLinuxBuilderConfig was missing the 'files' field, so
electron-builder used defaults that excluded transitive workspace deps
from the asar. Plus several other mac.ts patterns that I dropped from
the plan: artifactName, executableName, extraMetadata.main/name/
productName/version, npmRebuild=false, nodeGypRebuild=false,
buildDependenciesFromSource=false, compression=maximum, top-level icon.
Switch asar:true → asar:false to match mac.ts (easier to debug missing
files; perf difference negligible for dev installs).
* feat(tools-pack): implement linux install
* feat(tools-pack): implement linux start with extract-and-run
The packaged sidecar's 35-second wait timeout is exceeded when the
AppImage runs from a FUSE-mounted SquashFS (Node module loads + daemon
init are slow through FUSE). Pass --appimage-extract-and-run as the
first arg so AppImage extracts to /tmp first; subsequent file reads
go through a real filesystem and daemon boot completes in time.
Wait for apps/packaged to write desktop-root.json (60s ceiling, generous
to cover AppImage extraction overhead), then fetch desktop status via
sidecar IPC, return the merged LinuxStartResult.
* fix(tools-pack): align linux start helper with mac.ts (log echo + write semantics)
Code review flagged two unjustified divergences from mac.ts in
startPackedLinuxApp:
1. Missing OD_DESKTOP_LOG_ECHO=0 in spawn extraEnv. Without it the
packaged logger echoes to the spawned process's stdout, which goes
nowhere (logFd: null). Added the suppression to match mac.ts.
2. The desktop log truncate writeFile() was wrapped in .catch(() =>
undefined), silently swallowing fs errors that would later surface as
confusing missing-log symptoms. Removed the .catch so errors
propagate per mac.ts.
Also added an inline comment explaining the 60s waitForMarker timeout
(vs mac's tighter ceiling) so the rationale is preserved at the
call site.
* feat(tools-pack): implement linux stop with marker validation
* fix(tools-pack): align linux stop with mac.ts (graceful shutdown + reason strings)
Code review flagged divergences from mac.ts in stopPackedLinuxApp:
1. No graceful IPC SHUTDOWN attempt before SIGTERM/SIGKILL. Mac's
pattern lets Electron renderers + sidecars flush state (SQLite WAL,
logs) first. `gracefulRequested: true` was hardcoded, lying to
callers about what actually happened. Now attempts SHUTDOWN with a
1500ms timeout and reports the actual outcome.
2. The dead-PID-but-marker-exists branch returned reason 'ok' (the
neutral placeholder from readDesktopRootIdentityMarker), which says
nothing useful. Override to 'marker-pid-not-running' to match mac.ts.
3. After a clean stop, remove the desktop-root.json marker so a
subsequent start has a fresh slate (mac.ts does this too).
* fix(tools-pack): clear stale desktop-root.json before linux start
Smoke-testing the install/start/stop loop revealed waitForMarker
returns instantly when a stale marker from a previous run still exists
on disk (e.g., the previous AppImage was killed without going through
'tools-pack linux stop'). The start function then reports success
without actually waiting for the new spawn to write its own marker.
Defensively remove the marker file before spawning. mac.ts removes it
in stop, so a clean stop->start sequence has nothing to remove here.
This only matters for crash-recovery.
* fix(tools-pack): linux stop validates extract-and-run AppImages
Smoke testing exposed a gap from Task 7: matchesAppImageProcess only
recognized FUSE-mode (/tmp/.mount_*/AppRun) but Task 13 launches with
--appimage-extract-and-run, which puts the live executable at
/tmp/appimage_extracted_<hex>/<binary>. Stop's cmdOk validation
returned false, marker validation failed, the running app was
classified as 'unmanaged' and stop refused to kill it.
Fix:
1. matchesAppImageProcess accepts both runner patterns. Extract-and-run
regex matches /^\/tmp\/appimage_extracted_[^/]+\/[^/]+$/.
2. stopPackedLinuxApp now passes paths.installAppImagePath (or the
built fallback) as the canonical install path, not marker.appPath
(which apps/packaged unhelpfully writes as '/' on Linux).
3. linux.test.ts gains 2 new tests covering the extract-and-run mode
(both positive and the wrong-APPIMAGE-env negative case).
* fix(tools-pack): resolve linux paths in stop (typecheck regression from previous commit)
* feat(tools-pack): implement linux logs
* feat(tools-pack): implement linux uninstall
* feat(tools-pack): implement linux cleanup
* docs(tools-pack): document linux lane in READMEs and AGENTS files
* ci(release): add linux x64 AppImage to release-beta and release-stable
Mirrors the existing build_mac/build_win pattern with a build_linux job
in both release workflows. Builds via `tools-pack linux build
--containerized --to appimage` so the AppImage is linked against the
electronuserland/builder glibc 2.27 baseline (portable across distros)
rather than the ubuntu-latest glibc 2.39.
The linux asset is uploaded to the immutable version release tag
alongside mac/win. The beta channel-feed release (latest-mac.yml,
latest.yml) is intentionally not extended with latest-linux.yml because
tools/pack/src/linux.ts has no electron-builder publish block wired,
so the auto-update feed would point users at a feed that never updates.
AppImage auto-update is a separate follow-on.
Linux is unsigned (no signing path in tools-pack yet), so the beta
asset uses the .unsigned suffix matching the windows convention; the
stable asset uses no suffix, matching the stable windows convention.
* fix(tools-pack): propagate --dir/--portable into containerized linux build
The inner `pnpm tools-pack linux build` invocation in `buildDockerArgs`
only forwarded `--to` and `--namespace`. Callers passing `--dir` (e.g.
the new release workflows using `--dir $RUNNER_TEMP/tools-pack`) had
their flag silently dropped: the container defaulted to writing under
/project/.tmp/tools-pack while the host's `findBuiltAppImage` looked at
the caller's chosen `--dir`, producing "expected AppImage not found"
on any non-default tool-pack root. Callers passing `--portable` had
the same drop, baking build-machine runtime roots into shipped artifacts.
Fix:
- Mount `${config.roots.toolPackRoot}:/tools-pack` (new third volume,
alongside the existing /project, /home/builder, and cache mounts).
- Forward `--dir /tools-pack` to the inner build so its output lands
inside the mounted host dir.
- Forward `--portable` when `config.portable` is true.
The mount overlaps harmlessly with /project when toolPackRoot lives
under workspaceRoot (default case): Docker exposes the same host inode
at both paths. The existing .docker-home and .docker-cache/* mounts
continue to shadow the parent at their specific /home/builder paths.
Document the shell-interpolation safety invariant on the inner command:
config.namespace is sanitized at config-time, config.to is enum-validated,
config.portable is boolean -- none can carry shell metacharacters.
Tests: add coverage for the new /tools-pack mount, --dir forwarding,
and --portable propagation (both true and false branches).
Resolves the P1 review feedback from the Codex bot on PR #369.
* docs(tools-pack): polish linux README based on PR review
Addresses non-blocking P2/P3 review feedback on PR #369:
- AppImage launch mode: name the test distros (Ubuntu 24.04, Arch Linux)
and frame the FUSE-vs-extract-and-run gap as an order-of-magnitude
improvement instead of an unspecified slowdown.
- Optional system tools: add a libfuse2 paragraph distinguishing FUSE
launch (needs libfuse2) from extract-and-run (does not), with the
Ubuntu-24-vs-pre-24 package name caveat.
- New section "Format choice: why AppImage first" anchoring the
AppImage-only decision against industry precedent (VS Code, Discord,
Slack, Cursor, Obsidian) so the rationale survives without a reviewer.
- Out of scope: convert the dense one-liner into a bulleted list, mark
AppImage signing as gated on GPG infra + verification flow design
(no ETA), explain the latest-linux.yml gap, and remove the now-stale
"release lane" entry since this PR adds it.
* fix(tools-pack): add --appimage-extract-and-run to installed .desktop launcher
The XDG .desktop file installed by `tools-pack linux install` invoked
the AppImage directly via `Exec=env OD_NAMESPACE=<ns> <exec> %U`. That
bypassed the extract-and-run flag that `tools-pack linux start` applies,
so menu launches and `od://` desktop activations could hit the FUSE
slow path that was already shown to make the daemon sidecar exceed
apps/packaged's 35-second startup timeout. CLI-spawned starts succeeded
while menu-launched starts could fail with the same artifact.
Add `--appimage-extract-and-run` to the template's `Exec=` line and
update the renderDesktopTemplate test expectation. New regression test
locks the flag into place so a future template edit can't silently
drop it.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): treat signal-terminated container builds as failures
`runBuildInContainer` resolved the build promise on `code === null`,
which in Node's child-process `exit` event means the child was
terminated by a signal (SIGTERM, SIGKILL, OOM-killer, parent process
death). A killed Docker build could therefore make `packLinux` report
a containerized build as complete even though the artifact was
partial or missing.
Accept the `signal` argument on the exit handler. Resolve only when
`code === 0 && signal == null`. Otherwise reject with a message
naming either the non-zero code or the terminating signal so the
failure mode is visible in CI logs and `tools-pack linux build --json`
output.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): tear down orphaned process tree on failed linux start
If `startPackedLinuxApp` spawned the AppImage but the post-spawn
readiness path then failed -- either because the 60s waitForMarker
ceiling elapsed without the daemon writing desktop-root.json, or
because fetchDesktopStatus threw -- the detached child was left
running. Because the marker is the only persistent identity source
used by `stopPackedLinuxApp`, future lifecycle commands could not
associate the orphan with the namespace, leaving stale Electron and
sidecar processes plus stale IPC sockets that would interfere with
subsequent starts.
Wrap the readiness wait + status fetch in try/catch. On failure,
collect the spawned child's process tree via listProcessSnapshots +
collectProcessTreePids and stopProcesses() it (the same path
stopPackedLinuxApp uses for its tree teardown), then rethrow the
original error. Cleanup errors are swallowed so the original failure
is preserved in the rejection.
Extract the tree-teardown helper as `teardownOrphanedStart` so the
intent is documented at the call site without inlining 4 imports of
implementation detail.
Resolves a P2 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): use `corepack pnpm` in containerized linux build
The inner command in `buildDockerArgs` started with `corepack enable`,
which writes pnpm/yarn/npm shims into the directory containing the
node binary. In `electronuserland/builder:base`, that directory is
owned by root, but the container runs as the host's non-root uid via
`--user` (so build artifacts come out owned by the caller, not root).
The `corepack enable` step therefore fails with EACCES before
`pnpm install` ever runs, blocking the new release `build_linux` job
from publishing the Linux AppImage.
Switch to `corepack pnpm install --frozen-lockfile && corepack pnpm
tools-pack linux build ...`, which resolves and runs the version of
pnpm pinned in package.json's `packageManager` field directly. No
shims, no global mutation, no root writes — corepack just dispatches
to the pinned binary as the unprivileged user.
Update the existing inner-command test to match the new corepack
invocation, and add a regression test that asserts the inner command
contains `corepack pnpm` and never `corepack enable` so a future edit
can't reintroduce the root-write requirement.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): accept menu-launched processes in linux stop/uninstall
stopPackedLinuxApp validated the live process via matchesStampedProcess
against the process command line, requiring a SIDECAR_SOURCES.TOOLS_PACK
stamp. That worked for `tools-pack linux start` (which spawns with
createProcessStampArgs), but rejected menu launches: the installed
.desktop entry only sets OD_NAMESPACE and does not pass stamp args, so
apps/packaged falls back to a SIDECAR_SOURCES.PACKAGED stamp written
into desktop-root.json -- a perfectly valid identity, just not the one
the validator accepted.
Symptoms with the old behavior:
- `tools-pack linux stop` reported `unmanaged` for menu-launched apps
and refused to stop them.
- `tools-pack linux uninstall` would happily remove the AppImage,
.desktop entry, and icon while the packaged app was still running,
breaking handles to the AppImage's mounted/extracted contents.
Switch the validator to read marker.stamp directly (the file content
written by apps/packaged itself, not the process command) and accept
either TOOLS_PACK or PACKAGED. The expected app/mode/namespace/ipc
fields are still required to match. Mirrors the dual-source acceptance
pattern in mac.ts:709-714.
The matchesAppImageProcess (cmdOk) and namespaceRoot checks are
preserved -- the marker still has to point at our AppImage at a path
in our namespace's runtime root.
Drop the now-unused matchesStampedProcess import.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): per-platform --to help text in CLI
addBuildOptions is shared across mac/win/linux but its --to help text
hard-coded the mac targets (all|app|dmg|zip), so:
- tools-pack linux --help advertised --to all|app|dmg|zip even
though resolveToolPackBuildOutput accepts only all|appimage|dir,
sending users at invalid targets and hiding the AppImage option.
- tools-pack win --help had the same problem (advertised mac
targets while accepting all|dir|nsis with default nsis).
Parameterize addBuildOptions(command, platform) and back it with a
TO_HELP_BY_PLATFORM table that mirrors the resolver's accepted targets
in config.ts. Update the three call sites.
Smoke verified by running --help for each platform:
linux: all|appimage|dir (default: all)
mac: all|app|dmg|zip (default: all)
win: all|dir|nsis (default: nsis)
The misleading "--signed: build a signed/notarized mac artifact" line
on win/linux is left alone -- out of scope for this fix and not part
of the review feedback.
Resolves a P3 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): use OD_PACKAGED_NAMESPACE in installed .desktop launcher
The installed .desktop entry's Exec= line set OD_NAMESPACE=<ns>, but
apps/packaged/src/config.ts:9 reads namespace overrides from
OD_PACKAGED_NAMESPACE, not OD_NAMESPACE. The env assignment was a
silent no-op for menu launches: the packaged app fell back to whatever
namespace was baked into open-design-config.json at install time,
ignoring the namespace advertised in the .desktop file.
Practical effect: a .desktop launcher created for namespace "foo"
could end up running as the namespace baked into the AppImage's
shipped config (typically "default"), so installs created across
multiple namespaces could collide silently from menu launches. CLI
launches via `tools-pack linux start` were unaffected because they
pass the namespace through createSidecarLaunchEnv which targets the
correct env var.
Switch the template to OD_PACKAGED_NAMESPACE. Update the existing
renderDesktopTemplate test fixture/expectation, and add a regression
test that asserts the Exec= line uses OD_PACKAGED_NAMESPACE and never
the wrong OD_NAMESPACE name.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): gate linux uninstall + cleanup on stop status
uninstallPackedLinuxApp called stopPackedLinuxApp first, then deleted
the AppImage / .desktop entry / icon unconditionally. cleanupPackedLinux
Namespace did the same with the output and runtime namespace roots.
Both ignored stop.status -- so when stop returned "partial" (some
processes survived SIGTERM->SIGKILL) or "unmanaged" (the running PID
failed marker validation), uninstall would yank the install files out
from under a still-running packaged app, breaking handles to the
mounted/extracted AppImage contents and leaving an orphan with stale
SQLite WAL files / log handles / IPC sockets.
Extract a small `isSafeToRemoveInstallFiles(stop)` helper that returns
true only for "stopped" or "not-running". Both uninstall and cleanup
short-circuit when it returns false:
- uninstall reports "skipped-process-running" for each removal slot
and "skipped" for the post-install hooks. Existing "ok" / "already-
removed" / "ok"|"missing"|"failed" paths are unchanged.
- cleanup leaves both removed* booleans false and adds a new
`skipped: boolean` field set to true. Old consumers that only read
the booleans see the same "nothing was removed" signal they would
have seen for an already-clean namespace; new consumers can
distinguish "nothing to remove" from "refused to remove."
LinuxUninstallResult.removed.{appImage,desktop,icon} now also accepts
"skipped-process-running"; LinuxUninstallResult.postUninstall.* now
also accepts "skipped". LinuxCleanupResult gains the `skipped` field.
Workspace typecheck clean -- the only consumer is the CLI's printJson,
which doesn't constrain the wire shape.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
|
||
|
|
1edab990bb |
feat(craft): add brand-agnostic craft references + Refero-derived lint rules (#225)
* feat(craft): add brand-agnostic craft references and refero-derived lint rules Introduce `craft/` as a third top-level content axis alongside `skills/` and `design-systems/`, holding universal (brand-agnostic) craft rules that apply on top of any DESIGN.md. Skills opt in via a new `od.craft.requires` front-matter array; the daemon resolves the slug list and injects the matching files between DESIGN.md and the skill body in the system prompt. Initial vendor (MIT, adapted from referodesign/refero_skill): typography craft, color craft, anti-ai-slop. Pilot wired on saas-landing. Extend the existing lint-artifact pass with two refero-derived rules: - P0 ai-default-indigo — solid #6366f1 / #4f46e5 / #4338ca / #8b5cf6 as accent (not just gradients) is the most-reported AI tell. - P1 all-caps-no-tracking — `text-transform: uppercase` rules without ≥0.06em letter-spacing. The craft loader silently drops missing files so a skill can forward-reference future sections (e.g. `motion`) without breaking. * fix(daemon): skip :root token blocks in ai-default-indigo lint The ai-default-indigo P0 check scanned the whole HTML for the raw hex, so brands that intentionally encode indigo as `--accent: #6366f1` in :root and consume it via var(--accent) downstream were flagged as AI-default — a false positive that forced the agent to "fix" valid output. Strip :root token-definition blocks (including attribute-selector theme variants) before scanning, mirroring the existing pattern used by the raw-hex P1 check. Hex still flagged when it appears in component rules or inline styles. * docs(craft): address PR #225 P3 review feedback - craft/README.md: explain why missing craft sections are silently dropped (forward-compatibility) instead of surfacing a warning. - craft/typography.md: ground the 0.06em ALL CAPS tracking floor in Bringhurst-derived typographic practice rather than presenting the threshold as unattributed. - craft/color.md: cover the edge case where a brand's DESIGN.md intentionally encodes indigo as --accent — `var(--accent)` uses remain unflagged because the linter only inspects hardcoded hex. - docs/skills-protocol.md: link the "missing files dropped silently" note back to craft/README.md for the canonical slug list and the rationale behind the choice. * fix(craft): address PR #225 P0 review feedback - tools/pack: copy `craft/` into the packaged resource root alongside `skills`, `design-systems`, and `frames`, so the `od.craft.requires` integration isn't a silent no-op when the daemon resolves `${OD_RESOURCE_ROOT}/craft` in packaged builds. - packages/contracts: add `craftRequires?: string[]` to `SkillSummary` (and therefore `SkillDetail`) so the field that `listSkills()` already returns and `/api/skills(/:id)` already serializes via `...rest` is part of the documented web/daemon contract instead of leaking through as an untyped property. - apps/daemon/lint-artifact: expand the indigo token-strip pass to cover selector lists containing `:root` (e.g. `:root, [data-theme="light"]`) and any rule whose body is custom-property-only (e.g. a `[data-theme="dark"] { --accent: ... }` theme variant). Real component rules with a hardcoded indigo are still preserved so the P0 finding still fires; tests cover the new selector-list and theme-variant cases. * fix(craft): address PR #225 follow-up review feedback - lint-artifact: scope the indigo token-strip to <style> blocks so the rule-shaped regex no longer captures leading `<style>` text into the selector (which broke `:root` recognition for token blocks that mix `color-scheme`/etc. with `--accent`). Run the strip on the extracted CSS instead, with a regression covering `:root { color-scheme: light; --accent: #6366f1 }`. - lint-artifact: tighten the custom-property-only exemption to global theme-scope selectors (`:root`, `html`, `body`, bare attribute selectors like `[data-theme="dark"]`). Component-local rules such as `.cta { --cta-bg: #6366f1 }` are no longer exempted, so an agent cannot launder default indigo through a local var. Regression test added. - craft/anti-ai-slop.md: stop claiming every rule below is enforced by the linter; only several are. The unenforced rules (standard Hero→Features→Pricing→FAQ→CTA flow, decorative blob/wave SVG backgrounds, perfect symmetry) are now flagged inline as "(guidance, not auto-checked)" so the contract with the lint surface stays honest. * fix(daemon): tighten lint-artifact iteration and :root token gating - all-caps-no-tracking: iterate every <style> block. The previous check called `exec` once on a non-global regex, so an artifact whose offending uppercase rule sat in a second <style> block (e.g. a reset block followed by a components block) slipped past. Switch to `matchAll` and break across both loops once a violation is found. Regression test covers a second-block uppercase rule. - ai-default-indigo: stop unconditionally exempting any selector list containing `:root`. The exemption now requires both conditions to hold: every selector in the list is global theme scope AND the body is token-shaped (CSS custom properties or the `color-scheme` keyword). So `:root { background: #6366f1 }` and `:root, .cta { --cta-bg: #6366f1 }` no longer launder a hardcoded indigo through the strip pass. Regression tests cover both bypass shapes. * fix(daemon): scope theme-attr exemption and strip CSS comments in token blocks Address PR #225 review feedback on `ai-default-indigo`: - The bare-attribute branch of `selectorListIsGlobalThemeScope` accepted any `[attr=...]` selector, so a custom-property-only rule on a component/state attribute (e.g. `[data-variant="primary"]`, `[aria-current="page"]`) was treated as a global theme block and stripped before the indigo scan — exactly the component-local indigo laundering this lint is meant to catch. Restrict the exemption to a small allowlist of known theme switches: `data-theme`, `data-color-scheme`, `data-mode`. - `stripTokenBlocksFromCss` split rule bodies on `;` and matched each fragment from the start, so a token block whose body contained a normal CSS comment such as `:root { /* brand accent */ --accent: #6366f1; }` produced a fragment beginning with the comment, failed `isTokenShapedDeclaration`, and the rule was left in scope of the indigo scan — a false P0 on a legitimate token definition. Strip CSS comments before splitting/classifying declarations. Add regression coverage: arbitrary component/state attribute selectors still trip `ai-default-indigo`; `data-color-scheme` theme variants stay exempted; `:root` token blocks with leading, trailing, and between-declaration CSS comments are recognized. * fix(daemon): strip CSS comments and recognize tokens nested in at-rules The all-caps-no-tracking scan ran against raw `<style>` content, so a commented-out rule like `/* .eyebrow { text-transform: uppercase; } */` matched `upperRe` and emitted a P1 for CSS the browser ignores. Strip CSS comments from the style body before structural matching. `stripTokenBlocksFromCss` only matched flat `selector { body }` rules, so a media-query-wrapped token block like `@media (prefers-color-scheme: dark) { :root { --accent: #6366f1 } }` had its outer `@media` rule treated as the selector/body pair and the inner `:root` token block was never stripped, producing a P0 false positive on legitimate responsive theme CSS. Tighten the body alternation to `[^{}]*` so the regex matches innermost rules and recognizes the inner `:root` block directly while preserving the outer at-rule wrapper. * fix(daemon): align ai-default-indigo list with documented cardinal sins The lint's AI_DEFAULT_INDIGO subset omitted #3730a3 and #a855f7, which craft/anti-ai-slop.md lists as P0-blocked solid accents. An artifact could hard-code one of those documented colors as a button fill and slip past the indigo scan unless it happened to be inside a gradient. Bring the lint set to the exact list documented in the craft doc, and tighten the doc's wording from "etc." to an explicit enumeration that points at AI_DEFAULT_INDIGO so the prompt contract and daemon behavior stay in sync. Add regression tests pinning each newly-included hex. * fix(daemon): tighten theme-scope selector and scan inline ALL CAPS The theme-scope exemption used to accept any attribute on `:root`, `html`, or `body` (e.g. `:root[data-variant="primary"]`), letting an agent launder default indigo through a component/state attribute and slip past the `ai-default-indigo` lint. The prefixed branches now require the attribute name to be one of GLOBAL_THEME_ATTRIBUTES, matching the bare-attribute branch. The `all-caps-no-tracking` rule only iterated `<style>` blocks, so inline declarations like `<span style="text-transform: uppercase">` produced no finding even though craft/typography.md treats the ≥0.06em tracking floor as having no exceptions. Added a second scan over `style="..."` attributes that runs the same letter-spacing check and dedupes against the existing `<style>`-block finding so the agent gets a single corrective signal per artifact. * fix(daemon): align uppercase tracking px floor with the 0.06em rule The previous absolute fallback (>=1.5px) was stricter than the craft rule it enforces. `font-size: 12px; letter-spacing: 1px` is 0.083em — above the 0.06em floor — but 1.5px would reject it and trigger an unnecessary correction loop on compliant small-label CSS. Extract `hasAdequateUppercaseTracking`: read `font-size` from the same rule body and compare px tracking against `fontSize * 0.06`; fall back to a conservative >=1px floor when font-size is inherited (covers the default 16px body where 1px ≈ 0.0625em). Apply the helper to both the <style>-block scan and the inline-style scan, and add 12–14px label tests in both branches. * fix(daemon): treat rem letter-spacing as absolute, not per-element em `rem` was previously folded into the same branch as `em` and accepted at the 0.06 threshold. But `rem` is relative to the root font-size (16px default), not the element's own font-size, so on a 48px heading `letter-spacing: 0.06rem` resolves to 0.96px — about 0.02em of the element, well below the 0.06em rule the lint enforces. Convert rem to absolute px through the 16px root assumption and reuse the same px-vs-element-font-size resolution: same-rule `font-size: <n>px` gives an exact `n * 0.06` floor; otherwise the conservative >=1px fallback applies. Add regression tests for 48px headings with 0.06rem tracking (must flag) plus the 16px-element and rem-floor matches that must keep passing, in both <style>-block and inline-style branches. * fix(daemon): resolve var() refs in uppercase tracking lint `hasAdequateUppercaseTracking` only matched literal numeric values, so a tokenized rule like `letter-spacing: var(--caps-tracking)` — exactly the pattern the craft prompt steers artifacts toward — was falsely reported as `all-caps-no-tracking`. Extract `--name: value` declarations from global theme scopes (`:root`, `html`, theme-attribute selectors) once per artifact, then expand simple `var(--name)` (and `var(--name, fallback)`) references in the inspected rule body before applying the existing 0.06em / px-floor / rem-conversion logic. References without a matching token and no fallback stay in place, preserving the conservative "missing tracking" finding. * fix(daemon): resolve rem and var() font-size in uppercase tracking lint Previously the px-vs-element-font-size resolution only matched `font-size: <n>px`. Any rem-based or tokenized display size fell through to the lenient `>= 1px` body-text fallback, so an artifact emitting `.display { font-size: 3rem; text-transform: uppercase; letter-spacing: 1px; }` (a ~48px heading with a 2.88px floor) slipped past the lint that this helper exists to enforce. Resolve `rem` font-size via the same root-font assumption already used for tracking, and treat any explicitly declared but unresolvable unit (`em`, `%`, `calc(...)`, an unresolved `var(...)`) conservatively — refuse the lenient fallback so the rule must use either an `em` letter-spacing or a verifiable px/rem font-size. `var()` font-size declarations resolve through the existing `resolveCssVars` pass before the size scan runs, so the same fix catches the tokenized-display-size pattern (`--display-size: 3rem`). * fix(daemon): parse declarations to ignore custom-prop names in uppercase tracking lint The hasAdequateUppercaseTracking and resolveFontSizePx helpers used substring regexes against the rule body, so a token-name declaration such as `--letter-spacing: 0.08em` or `--display-font-size: 48px` could satisfy the `letter-spacing` / `font-size` checks even though it has no rendered effect — letting actual ALL-CAPS-without-tracking rules slip past the P1 lint. Parse the declaration list, compare exact property names, and skip declarations whose property starts with `--`. Adds regression tests covering token-name letter-spacing (style-block + inline) and a token-name font-size masking the bail-out branch. * fix(daemon): scope indigo token exemption to --accent only Previously stripTokenBlocksFromCss removed every custom-property-only global theme block before the ai-default-indigo scan, which let a laundered indigo token like `:root { --primary: #6366f1 }` consumed via `var(--primary)` slip past the lint. The craft contract is that the only escape hatch is encoding indigo as the design system's `--accent` token; any other token name is still the LLM-default color hidden behind an arbitrary name. Narrow the strip pass so a non-`--accent` token whose value carries an AI-default indigo hex keeps the rule in scope, and add regression tests for `--primary` / `--button-bg` global tokens feeding a CTA, including the at-rule and theme-attribute variants. * fix(daemon): model CSS cascade in tracking lint and detect blue→cyan trust gradients Address PR #225 review feedback (3 comments): - `letter-spacing` / `font-size` selection now picks the LAST matching declaration in the rule body, modeling CSS source-order cascade. `.eyebrow { letter-spacing: 0.08em; letter-spacing: 0.02em }` renders the noncompliant 0.02em the browser actually shows; the previous first-match behaviour silently passed it. - `extractCssTokens` now records every distinct value seen for a token across global theme scopes, and `hasAdequateUppercaseTracking` enumerates each combination so a default-theme value below the floor cannot be rescued by a scoped override that happened to be parsed later (`:root { --caps-tracking: 0.02em }` + `[data-theme="dark"] { --caps-tracking: 0.08em }` now fires). - New `trust-gradient` P0 rule pairs blue/sky tokens against cyan tokens in `linear-gradient(...)` bodies so `blue→cyan` two-stop trust gradients (documented as a cardinal sin in `craft/anti-ai-slop.md`) are actually enforced — both the hex form (`linear-gradient(90deg, #3b82f6, #06b6d4)`) and the keyword form (`linear-gradient(90deg, blue, cyan)`). Adds 11 regression tests covering each path (cascade override in <style> and inline form, font-size cascade shifting the floor, both orderings of the conflicting-token cascade, the don't-over-fire case when every theme value clears the floor, hex / keyword / sky variants of the trust gradient, and the don't-double-fire case when purple-gradient already caught a mixed gradient). * fix(daemon): apply per-scope cascade in extractCssTokens When the same CSS custom property is declared more than once inside a single rule body (e.g. `:root { --caps-tracking: 0.02em; --caps-tracking: 0.08em }`), CSS source-order cascade collapses to the last value; the earlier declaration never reaches any element. `extractCssTokens` was treating intra-scope duplicates as simultaneous theme alternatives, so `hasAdequateUppercaseTracking` enumerated the stale 0.02em and emitted a spurious all-caps-no-tracking finding. Collapse duplicate token declarations within a rule body to the last value before merging into the cross-scope distinct-value map. Cross-scope overrides (separate `:root` and `[data-theme]` rules) remain preserved as distinct values so the conservative theme-cascade check still fires when ANY applicable theme renders below the floor. * fix(daemon): scope tracking lint to innermost rules and per-theme tokens Restrict the upperRe body alternation to [^{}]* so the regex matches innermost CSS rules and skips at-rule wrappers — an outer @media or @supports could otherwise capture as a single rule whose selector was the at-rule and whose body began with the inner selector token, masking the same-rule font-size and letting noncompliant tracking on large headings slip through the lenient inherited-size fallback. Replace the by-name-distinct-values token map with per-scope token records and a buildResolvedThemes pass that materializes one effective map per theme. Paired token declarations now stay paired during evaluation, so theme variants like :root + [data-theme=dark] no longer generate cross-theme cartesian pairings (e.g. default-size + dark-track) that emit false positives on legitimate light/dark themes. --------- Co-authored-by: looper <looper@open-claude.dev> |
||
|
|
a40d817d28 |
Add mac packaged runtime and beta release flow (#170)
* feat(pack): add mac packaged runtime control plane * feat(pack): harden mac packaged runtime lifecycle Keep packaged state namespace-scoped, make daemon paths explicit through sidecar launch env, and add conservative desktop identity/logging fallbacks for local mac package validation. * feat(pack): add mac beta release flow * fix(pack): generate mac update feed fallback * fix(pack): write portable beta checksums * fix(pack): make beta artifacts portable * fix(pack): clean up mac install visuals * fix(pack): address packaged runtime review feedback |
||
|
|
9b57c22c38 | docs: add git commit co-author policy (#131) | ||
|
|
86c256ad56 | Improve tools-dev web startup flow (#128) | ||
|
|
c6d11018a0 |
Refresh desktop integration control plane (#123)
* feat(dev): add desktop tools-dev control plane * refactor(sidecar): split Open Design contracts Move Open Design-specific sidecar protocol definitions into @open-design/contracts so sidecar and platform can remain descriptor-driven primitives. * refactor(daemon): organize package sources Keep daemon app code, tests, and sidecar entrypoints in separate package directories so each layer can be built and verified independently. * chore(repo): streamline maintenance entrypoints Centralize agent guidance by directory and reduce root command chains while preserving the existing build scope. * docs: translate agent guidance to English * fix(sidecar): tolerate stale IPC sockets Remove stale Unix socket files only after confirming no listener is active, so tools-dev can restart after unclean shutdowns. |
||
|
|
56d08b8c5f | Add shared contracts and migrate project code to TypeScript (#118) | ||
|
|
950c0bfd32 | init AGENTS.md (#114) |