Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actions/commit/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ inputs:
description: Paths of JPEG, PNG or WebP files to attach, one per line, up to 3.
required: false
default: ''
ai:
description: Whether an AI agent generated this commit on your behalf.
required: false
default: 'false'
url:
description: The devnull to commit to.
required: false
Expand All @@ -32,5 +36,6 @@ runs:
DEVNULL_SSH_KEY: ${{ inputs.ssh-key }}
DEVNULL_REPLY_TO: ${{ inputs.reply-to }}
DEVNULL_IMAGES: ${{ inputs.images }}
DEVNULL_AI: ${{ inputs.ai }}
DEVNULL_URL: ${{ inputs.url }}
run: bash "$GITHUB_ACTION_PATH/commit.sh"
7 changes: 6 additions & 1 deletion .github/actions/commit/commit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ main() {
|| fail "Could not load the ssh-key input: it must be a private Ed25519 or RSA key without a passphrase."
unset DEVNULL_SSH_KEY

local arguments=()
local arguments=(--source ci)
case "${DEVNULL_AI:-false}" in
true) arguments+=(--ai) ;;
false) ;;
*) fail "The ai input must be true or false." ;;
esac
if [ -n "${DEVNULL_REPLY_TO:-}" ]; then
arguments+=(--reply "$DEVNULL_REPLY_TO")
fi
Expand Down
27 changes: 26 additions & 1 deletion KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This file documents the platform's features, stack, and stories. Feature entries
- [Following, Latest, and Trending logs](#following-latest-and-trending-logs)
- [Searching developers and commits](#searching-developers-and-commits)
- [Writing commits](#writing-commits)
- [Commit badges and visibility](#commit-badges-and-visibility)
- [Translating commits](#translating-commits)
- [Code, emoji, and mentions](#code-emoji-and-mentions)
- [Draft previews](#draft-previews)
Expand Down Expand Up @@ -184,6 +185,23 @@ Commits, replies, quotes, scheduling, terminal submissions, and app (signed API)

Sources: [composer](resources/js/components/commit-composer.tsx), [commit model](app/Models/Commit.php), [commit validation](app/Http/Requests/CreateCommitRequest.php), [text normalization](app/Services/CommitBodies.php), [submission throttle](app/Http/Middleware/ThrottleCommits.php).

### Commit badges and visibility

Small badges identify a commit's source or how its content was produced:

- **cli** means it was posted through the terminal client.
- **ci** means it was posted through a workflow. A workflow that invokes the terminal script gets **ci**, without a second **cli** badge.
- **ai** means an AI agent generated content on a person's behalf. It can appear alongside **cli** or **ci**.
- **bot** identifies Tom's and World's replies. Their replies are excluded from **ai**, and **bot** has no visibility setting.

Website, API, and native-app submissions have no separate source badges by default. Sources and AI content are declared by the submitting client, not guessed from the text. Existing commits and submissions from older clients remain unclassified.

Under **Settings → Visibility**, choose whether to show terminal, workflow, and AI-generated commits. All three are shown by default. These preferences belong to your account and apply across devices, including logs, profiles, search, starred commits, notifications, and signed API responses. A commit matching any category you hide is filtered, while your own commits remain visible to you.

Filtered top-level commits disappear from lists. In a thread, filtered replies keep their place, with a message explaining that your visibility settings hid them; visible replies beneath them remain accessible. Quoted commits and direct links show a placeholder too. Choose **Show reply** or **Show commit** to read that individual commit temporarily in a dialog without moving the surrounding thread or changing your saved preferences. These are personal reading preferences, not privacy controls: the public commit remains available to other people.

Sources: [commit model](app/Models/Commit.php), [visibility settings](resources/js/pages/commit-visibility/edit.tsx), [badges](resources/js/components/commit-channel-badges.tsx), [filtered commits](resources/js/components/filtered-commit.tsx), [visibility settings action](app/Actions/UpdateUserCommitVisibility.php).

Livestream history: the team settled on calling posts **commits** and their list a **log** on [day one at 2:56:00](https://www.youtube.com/watch?v=6zCNQa3hNm0&t=10560s). At [3:02:19](https://www.youtube.com/watch?v=6zCNQa3hNm0&t=10939s), they agreed to rename the post model too, rather than changing only the UI. These are naming decisions, not timestamps proving the rename had finished.

### Translating commits
Expand Down Expand Up @@ -446,6 +464,8 @@ Source: [shortcut reference](resources/js/components/keyboard-shortcuts-dialog.t

Settings → Terminal shows how to post using the `/commit` shell script. It signs the submission locally with an Ed25519 or RSA SSH key whose public half is on your GitHub account, as an authentication or a signing key, and /dev/null verifies that signature. No browser login or API token is required for that submission; your private key is not uploaded. The script can create commits, reply with `--reply` using a commit hash or link, and attach up to three images with `--image`, without descriptions.

Terminal submissions are labeled **cli** by default. Add **`--ai`** when an agent generates a commit on your behalf. Workflows that call the script directly should add **`--source ci`**; combine it with **`--ai`** when the workflow publishes generated content. Use **`--help`** to see the options. These declarations are signed with the commit.

Before posting from the terminal or a GitHub Action, accept the [commit rules](#moderation) by making your first submission through the web composer. Terminal posts then follow the same moderation and 10-per-minute submission limit as browser posts, and always need 1–4,000 characters of text, even with images. Their first link supplies the default link card. Review a downloaded script before running it, and never paste a private key into a commit.

Sources: [terminal instructions](resources/js/pages/terminal/show.tsx), [CLI routes](routes/api.php), [signature verification](app/Http/Middleware/AuthenticateSshSignature.php).
Expand All @@ -456,6 +476,8 @@ Livestream history: terminal posting was reported working on [day one at 6:32:47

Use `dev-null-sh/dev-null.sh/.github/actions/commit@production` to publish updates from a GitHub workflow, for example when a release is published. Settings → Terminal includes a workflow example. The action accepts a commit body, an SSH private key supplied through a GitHub Actions secret, an optional `reply-to` reference, optional image paths (up to three), and an optional `url` for another /dev/null instance.

The action labels its commits **ci** automatically. Set its optional **`ai`** input to **`'true'`** when an agent generated the content on your behalf; it defaults to **`'false'`**. Generated workflow content carries both **ci** and **ai**.

Generate a dedicated CI key, add its public half to GitHub as a signing or authentication key, and store the private half only in the repository's encrypted secrets. The action supports Ed25519 or RSA keys without a passphrase. It reports the created commit's link and whether it is pending review as a workflow annotation; it has no step outputs, and a refused submission fails the job. The usual moderation and submission limits still apply.

Sources: [workflow instructions](resources/js/pages/terminal/show.tsx), [GitHub action](.github/actions/commit/action.yml).
Expand Down Expand Up @@ -487,8 +509,11 @@ A signed request sees what the web shows you, such as your own pending commits.
- switch Pastéis mode and give pastéis;
- report commits;
- sync from GitHub and save your lane color;
- read and update your commit visibility preferences, and reveal an individually filtered commit;
- delete your account.

Signed commit creation accepts optional **`source`** (`cli` or `ci`) and **`is_ai`** (a boolean) declarations. Without a source declaration, API and app commits have no source badge. **bot** is reserved for Tom and World. Read or update the hidden categories through **`GET`** or **`PATCH /api/v1/me/commit-visibility`**, using **`hidden_commit_channels`** with any of `cli`, `ci`, and `ai`. A signed **`GET /api/v1/commits/{commit}/visibility`** reveals an individual filtered commit without changing those preferences; normal visibility and block rules still apply.

Signed request bodies are JSON, or multipart when they carry images or a voice note, so app commits can have images with descriptions or a poll but no link-card choice. Before your first app submission, accept the [commit rules](#moderation) through the web composer. Scheduling, search, and translations remain web-only. Signed requests are limited to **120 requests per minute per user**. Settings → Devices lists your approved devices with when each was last used, and revokes them.

Sources: [signed requests](app/Services/SignedRequests.php), [sign-in page](app/Http/Controllers/MobileAuthorizationController.php), [devices settings](resources/js/pages/device/index.tsx), [API routes](routes/api.php).
Expand Down Expand Up @@ -554,7 +579,7 @@ Ordinary photos of children are not prohibited just because a child appears in t

Posts awaiting automated checks stay pending if those checks fail; a failed check never approves a post.

Before you first commit, reply, quote, or schedule a commit, the web composer shows the commit rules, and keeps showing them on every attempt until you send with "Got it, commit": no engagement bait, no low-effort posts, no AI-generated posts, no spam or self-promotion, and no harassment, hate, or private information, followed by a link to the terms. Your first submission must come from the web so you can accept these rules; terminal, GitHub Action, and app posting become available afterwards.
Before you first commit, reply, quote, or schedule a commit, the web composer shows the commit rules, and keeps showing them on every attempt until you send with "Got it, commit": no engagement bait, no low-effort posts, no undisclosed AI-generated posts, no spam or self-promotion, and no harassment, hate, or private information, followed by a link to the terms. Disclose generated content rather than presenting it as your own writing. Your first submission must come from the web so you can accept these rules; terminal, GitHub Action, and app posting become available afterwards.

During a **timeout**, you can still read and react, but cannot commit, reply, quote, schedule, open issues, send remotes, or start, rename, add people to, or pin messages in orgs from any surface; each attempt says how long is left and why. Scheduled commits wait until the timeout ends. Like bans, timeouts belong to the GitHub account: deleting the /dev/null account and signing in again does not end a timeout or erase its history.

Expand Down
7 changes: 5 additions & 2 deletions app/Actions/CreateCommit.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Dto\NewPoll;
use App\Dto\ThreadEngagement;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Jobs\CheckUserLevel;
use App\Jobs\EmbedCommit;
Expand Down Expand Up @@ -47,20 +48,22 @@ public function __construct(
* @param list<UploadedFile> $images
* @param array<int, string|null> $imageDescriptions
*/
public function handle(User $user, string $body, ?Commit $parent = null, ?Commit $quotedCommit = null, array $images = [], array $imageDescriptions = [], bool|string $linkPreview = true, ?NewPoll $poll = null): Commit
public function handle(User $user, string $body, ?Commit $parent = null, ?Commit $quotedCommit = null, array $images = [], array $imageDescriptions = [], bool|string $linkPreview = true, ?NewPoll $poll = null, ?CommitSource $source = null, bool $isAi = false): Commit
{
$url = match ($linkPreview) {
true => $this->urls->first($body),
false => null,
default => $linkPreview,
};

return DB::transaction(function () use ($user, $body, $parent, $quotedCommit, $images, $imageDescriptions, $url, $poll): Commit {
return DB::transaction(function () use ($user, $body, $parent, $quotedCommit, $images, $imageDescriptions, $url, $poll, $source, $isAi): Commit {
$linkPreview = $this->linkPreviewFor($url);
$createdAt = now();
$hasGoodReputation = $user->hasGoodReputation();

$commit = $user->commits()->create([
'source' => $source,
'is_ai' => $isAi,
'body' => $this->emojiShortcodes->replace($body),
'mentions' => $this->mentionedUsers->get($body),
'parent_id' => $parent?->id,
Expand Down
7 changes: 5 additions & 2 deletions app/Actions/CreateExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* @phpstan-type UserExport array{
* schema_version: int,
* exported_at: string,
* profile: array<string, bool|int|string|null>,
* profile: array<string, bool|int|string|list<string>|null>,
* commits: Generator<int, array<string, mixed>>,
* polls: Generator<int, ExportPoll>,
* poll_votes: Generator<int, ExportPollVote>,
Expand Down Expand Up @@ -81,6 +81,7 @@ public function handle(User $user): array
'preferred_lane' => $user->preferred_lane,
'timezone' => $user->timezone,
'show_local_time' => $user->show_local_time,
'hidden_commit_channels' => $user->hidden_commit_channels,
'translation_language' => $user->translation_language,
'automatic_translation' => $user->automatic_translation ?? true,
'lane_color' => $user->lane_color,
Expand Down Expand Up @@ -166,7 +167,7 @@ private function commits(User $user): Generator
{
$commits = $user->commits()
->withTrashed()
->select(['id', 'parent_id', 'quoted_commit_id', 'body', 'status', 'pasteis_count', 'created_at', 'updated_at', 'deleted_at'])
->select(['id', 'parent_id', 'quoted_commit_id', 'body', 'status', 'source', 'is_ai', 'pasteis_count', 'created_at', 'updated_at', 'deleted_at'])
->with('images:id,commit_id')
->lazyById(500);

Expand All @@ -183,6 +184,8 @@ private function commits(User $user): Generator
'body' => $commit->body,
'cached_translations' => $cached[$translations->key($commit)],
'status' => $commit->status->value,
'source' => $commit->source?->value,
'is_ai' => $commit->is_ai,
'pasteis_count' => $commit->pasteis_count,
'images' => $commit->images->map(fn (CommitImage $image): string => $image->url())->all(),
'created_at' => $commit->created_at->toIso8601String(),
Expand Down
2 changes: 1 addition & 1 deletion app/Actions/DeleteCommitContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function handle(Commit $commit): void

LinkPreview::query()->where('url', 'like', "%/commits/{$commit->id}%")->delete();

$commit->update(['body' => '', 'mentions' => null, 'embedding' => null, 'link_preview_id' => null]);
$commit->update(['body' => '', 'source' => null, 'is_ai' => false, 'mentions' => null, 'embedding' => null, 'link_preview_id' => null]);

return $imagePaths;
});
Expand Down
2 changes: 1 addition & 1 deletion app/Actions/DeleteUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function handle(User $user): void
}

$user->commits()->delete();
$user->commits()->withTrashed()->update(['user_id' => null, 'body' => '', 'mentions' => null, 'embedding' => null, 'link_preview_id' => null]);
$user->commits()->withTrashed()->update(['user_id' => null, 'body' => '', 'source' => null, 'is_ai' => false, 'mentions' => null, 'embedding' => null, 'link_preview_id' => null]);

$this->deleteOrgs->handle($this->emptyOrgs($user));

Expand Down
18 changes: 18 additions & 0 deletions app/Actions/UpdateUserCommitVisibility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\User;

final readonly class UpdateUserCommitVisibility
{
/**
* @param list<string> $hiddenCommitChannels
*/
public function handle(User $user, array $hiddenCommitChannels): void
{
$user->update(['hidden_commit_channels' => $hiddenCommitChannels]);
}
}
11 changes: 11 additions & 0 deletions app/Enums/CommitSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum CommitSource: string
{
case Cli = 'cli';
case Ci = 'ci';
}
3 changes: 2 additions & 1 deletion app/Http/Controllers/Api/CommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Actions\CreateCommit;
use App\Actions\DeleteCommit;
use App\Enums\CommitSource;
use App\Http\Requests\CreateApiCommitRequest;
use App\Http\Requests\DeleteApiCommitRequest;
use App\Http\Requests\ShowApiCommitRequest;
Expand Down Expand Up @@ -45,7 +46,7 @@ public function show(ShowApiCommitRequest $request, #[CurrentUser] ?User $curren
*/
public function store(CreateApiCommitRequest $request, #[CurrentUser] User $user, CreateCommit $action, CommitPageQuery $page): JsonResponse
{
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), $request->quotedCommit(), $request->images(), $request->imageDescriptions(), poll: $request->poll());
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), $request->quotedCommit(), $request->images(), $request->imageDescriptions(), poll: $request->poll(), source: $request->enum('source', CommitSource::class), isAi: $request->boolean('is_ai'));

return CommitResource::make($page->get($commit, $user))
->response()
Expand Down
24 changes: 24 additions & 0 deletions app/Http/Controllers/Api/CommitVisibilityController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Requests\ShowApiCommitVisibilityRequest;
use App\Http\Resources\CommitResource;
use App\Models\Commit;
use App\Models\User;
use App\Queries\CommitPageQuery;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\JsonResponse;

final readonly class CommitVisibilityController
{
public function show(ShowApiCommitVisibilityRequest $request, #[CurrentUser] User $user, Commit $commit, CommitPageQuery $page): JsonResponse
{
$shown = $page->get($commit, $user);
$shown->isFiltered = false;

return CommitResource::make($shown)->response($request)->header('Cache-Control', 'private, no-store');
}
}
3 changes: 1 addition & 2 deletions app/Http/Controllers/Api/NotificationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use App\Http\Resources\NotificationResource;
use App\Models\Notification;
use App\Models\User;
use App\Queries\CommitBuilder;
use App\Queries\NotificationGroupsQuery;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Database\Eloquent\Collection;
Expand All @@ -36,7 +35,7 @@ public function index(ListNotificationsRequest $request, #[CurrentUser] User $us
new Collection($items)->loadMissing([
'commit.user',
'commit.parent' => fn (BelongsTo $query): BelongsTo => $query
->where(fn (CommitBuilder $query): CommitBuilder => $query->visibleTo($user))
->visibleTo($user)->withVisibilityFor($user)
->with('user'),
]);

Expand Down
3 changes: 1 addition & 2 deletions app/Http/Controllers/Api/NotificationGroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use App\Http\Resources\NotificationResource;
use App\Models\Notification;
use App\Models\User;
use App\Queries\CommitBuilder;
use App\Queries\NotificationGroupsQuery;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand All @@ -26,7 +25,7 @@ public function show(ShowNotificationGroupRequest $request, #[CurrentUser] User
->with([
'commit.user',
'commit.parent' => fn (BelongsTo $query): BelongsTo => $query
->where(fn (CommitBuilder $query): CommitBuilder => $query->visibleTo($user))
->visibleTo($user)->withVisibilityFor($user)
->with('user'),
])
->latest()
Expand Down
26 changes: 26 additions & 0 deletions app/Http/Controllers/Api/UserCommitVisibilityController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Actions\UpdateUserCommitVisibility;
use App\Http\Requests\UpdateApiUserCommitVisibilityRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\JsonResponse;

final readonly class UserCommitVisibilityController
{
public function show(#[CurrentUser] User $user): JsonResponse
{
return response()->json(['hidden_commit_channels' => $user->hidden_commit_channels]);
}

public function update(UpdateApiUserCommitVisibilityRequest $request, #[CurrentUser] User $user, UpdateUserCommitVisibility $action): JsonResponse
{
$action->handle($user, $request->channels());

return response()->json(['hidden_commit_channels' => $user->hidden_commit_channels]);
}
}
Loading
Loading