wellactually

Writing principles

Sync from GitHub

Keep your principles in a public GitHub repository, and every push builds, tests and releases the ones that changed. This page says exactly what the registry looks for in the repository.

Name it principles#

Call the repository principles, so it reads as you/principles and looks like every other expert's. Give it the GitHub topic wellactually-principles, so people find it by the topic rather than by a name many unrelated repositories share. A second repository, or one in an organization, adds what sets it apart: principles-typescript, acme/principles-mobile. The registry reads any name; this is a convention, not a rule.

A repository that works#

This is the layout of passsy/principles, which holds 81 principles:

package.json              the root: workspaces, the SDK and vitest for npm test
vitest.config.ts
tsconfig.json
principles/
  avoid-late/
    principle.md          required: the advice
    detector.ts           optional: when the advice applies
    detector.test.ts      required with a detector: at least one *.test.ts
    package.json          optional: tags and dependencies
    fixtures/
      dart_app/
        pubspec.yaml      optional: projects the tests read
  use-clock/
    ...
packages/
  dart-kit/
    package.json          { "name": "dart-kit" }
    src/index.ts          shared code, TypeScript is fine

What makes a folder a principle#

The registry reads the whole repository at one commit and looks at every file path.

  1. A folder is a principle when it holds a principle.md. Its detector, detector.ts, detector.js or detector.mjs, is optional. A principle.md inside another principle's folder belongs to that principle.
  2. Where the folder lies does not matter. principles/avoid-late, dart/avoid-late and avoid-late at the root all work.
  3. Folders named node_modules, fixtures, .git, dist or build are never searched, at any depth. A principle inside build/ is never found.
  4. The folder's name is the principle's name, and its id is <your handle>/<folder name>. It has to be 3 to 64 characters of a-z, 0-9 and dashes, starting and ending with a letter or digit.
  5. Folder names have to be unique in the repository. When two folders share a name, the first in path order is the principle, and the other is reported and left out.
  6. A repository may hold up to 300 principles.

Everything inside the folder belongs to the principle: its tests, its fixtures, relative files its detector imports, its package.json and its LICENSE. A node_modules inside it is left out.

What a principle needs to pass#

A synced principle is built and checked exactly like an upload:

  • principle.md starts with a # heading, followed by a one-sentence summary of at most 200 characters, followed by the advice. See Writing the advice.
  • detector.ts registers at least one handler. See Detectors.
  • At least one *.test.ts outside fixtures, whose tests pass, make the detector fire at least once, and see it stay quiet at least once. See Testing.
  • A principle without a detector needs neither, and may have no tests. See Advice without a detector.
  • At most 60 files and 512 KB of its own, see Limits.

A principle that fails is listed on the repository's page with the reasons, and keeps the revision it had. The other principles are not held back.

package.json of a principle#

principles/avoid-late/package.json
{
  "name": "avoid-late",
  "private": true,
  "type": "module",
  "tags": ["dart", "null safety"],
  "dependencies": {
    "dart-kit": "*"
  }
}
FieldWhat the registry does with it
tagsUp to 5 tags of at most 30 characters. They are the principle's tags, and the website cannot change them.
dependenciesEvery package the detector imports: from npm, from GitHub, or from the same repository.
visibilityOptional, "public" or "private". When it is there, it sets the visibility and the website cannot change it.
forkableOptional, true or false. When it is there, it sets whether others may fork the principle and the website cannot change it.
retiredOptional, the reason it is retired. When it is there, the principle is retired; without it the website decides.
name, private, typeNothing. They are for npm workspaces on your machine.

devDependencies are not read. Tags, packages and licences has the details.

Licences of a synced principle#

The licences are the same as for any principle: the advice stays yours, all rights reserved, and the principle's own code is MIT. A LICENSE file in the principle's folder, or at the top of the repository, changes neither. The one at the top still counts for a package in the same repository.

Whether others may fork it comes from forkable in its package.json, or from the box in the Settings tab of its page when that field is not there.

Shared code in the same repository#

A detector can import a package that lives in the same repository, by its name.

  • A package is any folder with a package.json that has a name, outside the ignored folders, and that is not the root or a principle.
  • The principle asks for it in its own package.json, with any version: { "dependencies": { "dart-kit": "*" } }. A package of the repository wins over one of the same name on npm.
  • A package may depend on other packages of the repository, through the dependencies of its own package.json.
  • It can be TypeScript. The build compiles it together with the detector, so it needs no build step.
  • A change to the package rebuilds the principles that imported it, and only those.

The root package.json is not read by the registry. Making packages/* and principles/* npm workspaces is what makes npm install link the shared package on your machine, so that the tests and the agent's run_tests find it there too:

package.json
{
  "name": "principles",
  "private": true,
  "type": "module",
  "workspaces": ["packages/*", "principles/*"],
  "scripts": {
    "test": "vitest run",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "@types/node": "^24.0.0",
    "@wellactually/sdk": "github:passsy/wellactually#sdk",
    "typescript": "^5.9.0",
    "vitest": "^4.1.0"
  }
}

Collections live on the website#

A repository holds principles only. To group them, create a collection on the website and add the synced principles to it, see Collections and teams.

Connect the repository#

  1. Open your profile with your handle at the top right, then Repositories.
  2. Choose Connect next to the repository, from the public repositories the Well Actually app is installed on. To connect one the app is not on yet, type owner/repo instead, and a branch when it should not be the default branch. Only public repositories work: the registry holds no key to anyone's code.
  3. Read it once with Sync now. The page lists every principle it found, with its revision or the reasons it failed.
  4. Install the Well Actually app on it, from the link on the page, if it is not on the repository yet. It tells the registry about every push. On a registry without the app, the page shows a webhook to add on GitHub by hand.
  5. Push once to try it. The step ticks when the push arrives.

An account can connect up to 10 repositories. Without the app or a webhook, choose Sync now after you push.

What a sync does#

  • It reads the branch at one commit.
  • It builds only the principles whose files changed, or whose packages from the same repository changed.
  • Each new revision records the commit.
  • It releases what passed, unless the repository is set to Leave drafts instead. Then each change waits as a draft, and you release it on the principle's page. Switching back to Release on sync releases the waiting drafts with the next push.

Connecting the repository is the decision to run what is in it. Everything a push releases runs in your own agent and in the agents of everyone who has you on their advisory board.

Synced principles change only in the repository#

While the repository is connected, it is the only place its principles change.

  • The website offers no editing for their files, tags included.
  • Their visibility and whether they are retired can still be set on their pages, unless visibility or retired in their package.json sets them.
  • Their pages have an Edit on GitHub button instead, which opens the file in the repository, so anyone can propose a change as a pull request.
  • The agent's publish_draft refuses them, because the next push would overwrite the upload.

A folder with the name of a principle you made on the website takes that principle over: the sync becomes its next revision, and from then on it is read-only on the website.

Removing and disconnecting#

  • Removing a folder retires its principle with the next sync. It stops running for everyone, the people who ran it read that its folder was removed, and its page stays.
  • A folder that comes back with the same name, in any place in the repository, brings its principle back to life. A principle is known by its name, not by its path, so moving a folder keeps its history. A principle you retired yourself on its page stays retired, and one whose package.json says retired stays retired until that line goes.
  • Renaming a folder is a new principle with the new name, and the old one is retired.
  • Disconnecting the repository keeps every principle and revision it made. They keep running, and they can be edited on the website again.