add documentation for ai agents (#3115)

* add documentation for ai agents

* docs: consolidate CLAUDE.md and CONTRIBUTING.md, add style guidelines

- Refactor CLAUDE.md to reference CONTRIBUTING.md for build/test/format info
- Expand CONTRIBUTING.md with comprehensive build targets, env vars, and testing
- Add code style guidelines section with conventional commits
- Standardize SDK prettier config to use single quotes (matching web)
- Add project-level Claude Code settings to disable co-author attribution

* style(sdk): apply prettier with single quotes

Run prettier across sdk/base and sdk/package to apply the
standardized quote style (single quotes matching web).

* docs: add USER.md for per-developer TODO filtering

- Add agents/USER.md to .gitignore (contains user identifier)
- Document session startup flow in CLAUDE.md:
  - Create USER.md if missing, prompting for identifier
  - Filter TODOs by @username tags
  - Offer relevant TODOs on session start

* docs: add i18n documentation task to agent TODOs

* docs: document i18n ID patterns in core/

Add agents/i18n-patterns.md covering rust-i18n setup, translation file
format, t!() macro usage, key naming conventions, and locale selection.
Remove completed TODO item and add reference in CLAUDE.md.

* chore: clarify that all builds work on any OS with Docker
This commit is contained in:
Aiden McClelland
2026-02-06 00:10:16 +01:00
committed by GitHub
parent 86ca23c093
commit f2142f0bb3
280 changed files with 6793 additions and 5515 deletions

View File

@@ -1,12 +1,12 @@
import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types"
import { DEFAULT_SIGTERM_TIMEOUT } from '.'
import { NO_TIMEOUT, SIGTERM } from '../../../base/lib/types'
import * as T from "../../../base/lib/types"
import { SubContainer } from "../util/SubContainer"
import { Drop, splitCommand } from "../util"
import * as cp from "child_process"
import * as fs from "node:fs/promises"
import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from "./Daemons"
import * as T from '../../../base/lib/types'
import { SubContainer } from '../util/SubContainer'
import { Drop, splitCommand } from '../util'
import * as cp from 'child_process'
import * as fs from 'node:fs/promises'
import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from './Daemons'
export class CommandController<
Manifest extends T.SDKManifest,
@@ -31,7 +31,7 @@ export class CommandController<
exec: DaemonCommandType<Manifest, C>,
) => {
try {
if ("fn" in exec) {
if ('fn' in exec) {
const abort = new AbortController()
const cell: { ctrl: CommandController<Manifest, C> } = {
ctrl: new CommandController<Manifest, C>(
@@ -63,9 +63,9 @@ export class CommandController<
if (T.isUseEntrypoint(exec.command)) {
const imageMeta: T.ImageMetadata = await fs
.readFile(`/media/startos/images/${subcontainer!.imageId}.json`, {
encoding: "utf8",
encoding: 'utf8',
})
.catch(() => "{}")
.catch(() => '{}')
.then(JSON.parse)
commands = imageMeta.entrypoint ?? []
commands = commands.concat(
@@ -85,21 +85,21 @@ export class CommandController<
env: exec.env,
user: exec.user,
cwd: exec.cwd,
stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit",
stdio: exec.onStdout || exec.onStderr ? 'pipe' : 'inherit',
})
}
if (exec.onStdout) childProcess.stdout?.on("data", exec.onStdout)
if (exec.onStderr) childProcess.stderr?.on("data", exec.onStderr)
if (exec.onStdout) childProcess.stdout?.on('data', exec.onStdout)
if (exec.onStderr) childProcess.stderr?.on('data', exec.onStderr)
const state = { exited: false }
const answer = new Promise<null>((resolve, reject) => {
childProcess.on("exit", (code) => {
childProcess.on('exit', (code) => {
state.exited = true
if (
code === 0 ||
code === 143 ||
(code === null && childProcess.signalCode == "SIGTERM")
(code === null && childProcess.signalCode == 'SIGTERM')
) {
return resolve(null)
}
@@ -142,7 +142,7 @@ export class CommandController<
new Promise((_, reject) =>
setTimeout(
() =>
reject(new Error("Timed out waiting for js command to exit")),
reject(new Error('Timed out waiting for js command to exit')),
timeout * 2,
),
),
@@ -151,7 +151,7 @@ export class CommandController<
} finally {
if (!this.state.exited) {
if (this.process instanceof AbortController) this.process.abort()
else this.process.kill("SIGKILL")
else this.process.kill('SIGKILL')
}
await this.subcontainer?.destroy()
}
@@ -161,10 +161,10 @@ export class CommandController<
if (!this.state.exited) {
if (this.process instanceof AbortController) return this.process.abort()
if (signal !== "SIGKILL") {
if (signal !== 'SIGKILL') {
setTimeout(() => {
if (this.process instanceof AbortController) this.process.abort()
else this.process.kill("SIGKILL")
else this.process.kill('SIGKILL')
}, timeout)
}
if (!this.process.kill(signal)) {
@@ -180,7 +180,7 @@ export class CommandController<
new Promise((_, reject) =>
setTimeout(
() =>
reject(new Error("Timed out waiting for js command to exit")),
reject(new Error('Timed out waiting for js command to exit')),
timeout * 2,
),
),

View File

@@ -1,14 +1,14 @@
import * as T from "../../../base/lib/types"
import { asError } from "../../../base/lib/util/asError"
import { Drop } from "../util"
import * as T from '../../../base/lib/types'
import { asError } from '../../../base/lib/util/asError'
import { Drop } from '../util'
import {
SubContainer,
SubContainerOwned,
SubContainerRc,
} from "../util/SubContainer"
import { CommandController } from "./CommandController"
import { DaemonCommandType } from "./Daemons"
import { Oneshot } from "./Oneshot"
} from '../util/SubContainer'
import { CommandController } from './CommandController'
import { DaemonCommandType } from './Daemons'
import { Oneshot } from './Oneshot'
const TIMEOUT_INCREMENT_MS = 1000
const MAX_TIMEOUT_MS = 30000
@@ -87,7 +87,7 @@ export class Daemon<
try {
fn(success)
} catch (e) {
console.error("EXIT handler", e)
console.error('EXIT handler', e)
}
}
if (success && this.oneshot) {

View File

@@ -1,20 +1,20 @@
import { Signals } from "../../../base/lib/types"
import { Signals } from '../../../base/lib/types'
import { HealthCheckResult } from "../health/checkFns"
import { HealthCheckResult } from '../health/checkFns'
import { Trigger } from "../trigger"
import * as T from "../../../base/lib/types"
import { SubContainer } from "../util/SubContainer"
import { Trigger } from '../trigger'
import * as T from '../../../base/lib/types'
import { SubContainer } from '../util/SubContainer'
import { promisify } from "node:util"
import * as CP from "node:child_process"
import { promisify } from 'node:util'
import * as CP from 'node:child_process'
export { Daemon } from "./Daemon"
export { CommandController } from "./CommandController"
import { EXIT_SUCCESS, HealthDaemon } from "./HealthDaemon"
import { Daemon } from "./Daemon"
import { CommandController } from "./CommandController"
import { Oneshot } from "./Oneshot"
export { Daemon } from './Daemon'
export { CommandController } from './CommandController'
import { EXIT_SUCCESS, HealthDaemon } from './HealthDaemon'
import { Daemon } from './Daemon'
import { CommandController } from './CommandController'
import { Oneshot } from './Oneshot'
export const cpExec = promisify(CP.exec)
export const cpExecFile = promisify(CP.execFile)
@@ -231,7 +231,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
const res = (options: AddDaemonParams<Manifest, Ids, Id, C> | null) => {
if (!options) return prev
const daemon =
"daemon" in options
'daemon' in options
? options.daemon
: Daemon.of<Manifest>()<C>(
this.effects,
@@ -369,8 +369,8 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
const healthDaemon = new HealthDaemon<Manifest>(
daemon,
[...this.healthDaemons],
"__RUN_UNTIL_SUCCESS",
"EXIT_SUCCESS",
'__RUN_UNTIL_SUCCESS',
'EXIT_SUCCESS',
this.effects,
)
const daemons = await new Daemons<Manifest, Ids>(this.effects, this.ids, [
@@ -400,7 +400,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
if (canShutdown.length === 0) {
// Dependency cycle that should not happen, just shutdown remaining daemons
console.warn(
"Dependency cycle detected, shutting down remaining daemons",
'Dependency cycle detected, shutting down remaining daemons',
)
canShutdown.push(...[...remaining].reverse())
}

View File

@@ -1,8 +1,8 @@
import { HealthCheckResult } from "../health/checkFns"
import { defaultTrigger } from "../trigger/defaultTrigger"
import { Ready } from "./Daemons"
import { Daemon } from "./Daemon"
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
import { HealthCheckResult } from '../health/checkFns'
import { defaultTrigger } from '../trigger/defaultTrigger'
import { Ready } from './Daemons'
import { Daemon } from './Daemon'
import { SetHealth, Effects, SDKManifest } from '../../../base/lib/types'
const oncePromise = <T>() => {
let resolve: (value: T) => void
@@ -12,7 +12,7 @@ const oncePromise = <T>() => {
return { resolve: resolve!, promise }
}
export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
export const EXIT_SUCCESS = 'EXIT_SUCCESS' as const
/**
* Wanted a structure that deals with controlling daemons by their health status
@@ -22,7 +22,7 @@ export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
*
*/
export class HealthDaemon<Manifest extends SDKManifest> {
private _health: HealthCheckResult = { result: "waiting", message: null }
private _health: HealthCheckResult = { result: 'waiting', message: null }
private healthWatchers: Array<() => unknown> = []
private running = false
private started?: number
@@ -102,21 +102,21 @@ export class HealthDaemon<Manifest extends SDKManifest> {
}
private async setupHealthCheck() {
this.daemon?.onExit((success) => {
if (success && this.ready === "EXIT_SUCCESS") {
this.setHealth({ result: "success", message: null })
if (success && this.ready === 'EXIT_SUCCESS') {
this.setHealth({ result: 'success', message: null })
} else if (!success) {
this.setHealth({
result: "failure",
result: 'failure',
message: `${this.id} daemon crashed`,
})
} else if (!this.daemon?.isOneshot()) {
this.setHealth({
result: "failure",
result: 'failure',
message: `${this.id} daemon exited`,
})
}
})
if (this.ready === "EXIT_SUCCESS") return
if (this.ready === 'EXIT_SUCCESS') return
if (this.healthCheckCleanup) return
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
lastResult: this._health.result,
@@ -127,7 +127,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
}>()
const { promise: exited, resolve: setExited } = oncePromise<null>()
new Promise(async () => {
if (this.ready === "EXIT_SUCCESS") return
if (this.ready === 'EXIT_SUCCESS') return
for (
let res = await Promise.race([status, trigger.next()]);
!res.done;
@@ -137,8 +137,8 @@ export class HealthDaemon<Manifest extends SDKManifest> {
this.ready.fn(),
).catch((err) => {
return {
result: "failure",
message: "message" in err ? err.message : String(err),
result: 'failure',
message: 'message' in err ? err.message : String(err),
}
})
@@ -166,23 +166,23 @@ export class HealthDaemon<Manifest extends SDKManifest> {
private async setHealth(health: HealthCheckResult) {
const changed = this._health.result !== health.result
this._health = health
if (this.resolveReady && health.result === "success") {
if (this.resolveReady && health.result === 'success') {
this.resolveReady()
}
if (changed) this.healthWatchers.forEach((watcher) => watcher())
if (this.ready === "EXIT_SUCCESS") return
if (this.ready === 'EXIT_SUCCESS') return
const display = this.ready.display
if (!display) {
return
}
let result = health.result
if (
result === "failure" &&
result === 'failure' &&
this.started &&
performance.now() - this.started <= (this.ready.gracePeriod ?? 10_000)
)
result = "starting"
if (result === "failure") {
result = 'starting'
if (result === 'failure') {
console.error(`Health Check ${this.id} failed:`, health.message)
}
await this.effects.setHealth({
@@ -197,10 +197,10 @@ export class HealthDaemon<Manifest extends SDKManifest> {
const healths = this.dependencies.map((d) => ({
health: d.running && d._health,
id: d.id,
display: typeof d.ready === "object" ? d.ready.display : null,
display: typeof d.ready === 'object' ? d.ready.display : null,
}))
const waitingOn = healths.filter(
(h) => !h.health || h.health.result !== "success",
(h) => !h.health || h.health.result !== 'success',
)
if (waitingOn.length)
console.debug(
@@ -210,10 +210,10 @@ export class HealthDaemon<Manifest extends SDKManifest> {
const waitingOnNames = waitingOn.flatMap((w) =>
w.display ? [w.display] : [],
)
const message = waitingOnNames.length ? waitingOnNames.join(", ") : null
await this.setHealth({ result: "waiting", message })
const message = waitingOnNames.length ? waitingOnNames.join(', ') : null
await this.setHealth({ result: 'waiting', message })
} else {
await this.setHealth({ result: "starting", message: null })
await this.setHealth({ result: 'starting', message: null })
}
await this.changeRunning(!waitingOn.length)
}

View File

@@ -1,5 +1,5 @@
import * as T from "../../../base/lib/types"
import { IdMap, MountOptions } from "../util/SubContainer"
import * as T from '../../../base/lib/types'
import { IdMap, MountOptions } from '../util/SubContainer'
type MountArray = { mountpoint: string; options: MountOptions }[]
@@ -13,7 +13,7 @@ type SharedOptions = {
*
* defaults to "directory"
* */
type?: "file" | "directory" | "infer"
type?: 'file' | 'directory' | 'infer'
// /**
// * Whether to map uids/gids for the mount
// *
@@ -35,16 +35,16 @@ type SharedOptions = {
type VolumeOpts<Manifest extends T.SDKManifest> = {
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest */
volumeId: Manifest["volumes"][number]
volumeId: Manifest['volumes'][number]
/** Whether or not the resource should be readonly for this subcontainer */
readonly: boolean
} & SharedOptions
type DependencyOpts<Manifest extends T.SDKManifest> = {
/** The ID of the dependency */
dependencyId: Manifest["id"]
dependencyId: Manifest['id']
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest of the dependency */
volumeId: Manifest["volumes"][number]
volumeId: Manifest['volumes'][number]
/** Whether or not the resource should be readonly for this subcontainer */
readonly: boolean
} & SharedOptions
@@ -126,11 +126,11 @@ export class Mounts<
this.volumes.map((v) => ({
mountpoint: v.mountpoint,
options: {
type: "volume",
type: 'volume',
volumeId: v.volumeId,
subpath: v.subpath,
readonly: v.readonly,
filetype: v.type ?? "directory",
filetype: v.type ?? 'directory',
idmap: [],
},
})),
@@ -139,9 +139,9 @@ export class Mounts<
this.assets.map((a) => ({
mountpoint: a.mountpoint,
options: {
type: "assets",
type: 'assets',
subpath: a.subpath,
filetype: a.type ?? "directory",
filetype: a.type ?? 'directory',
idmap: [],
},
})),
@@ -150,12 +150,12 @@ export class Mounts<
this.dependencies.map((d) => ({
mountpoint: d.mountpoint,
options: {
type: "pointer",
type: 'pointer',
packageId: d.dependencyId,
volumeId: d.volumeId,
subpath: d.subpath,
readonly: d.readonly,
filetype: d.type ?? "directory",
filetype: d.type ?? 'directory',
idmap: [],
},
})),
@@ -163,6 +163,6 @@ export class Mounts<
}
}
const a = Mounts.of().mountBackups({ subpath: null, mountpoint: "" })
const a = Mounts.of().mountBackups({ subpath: null, mountpoint: '' })
// @ts-expect-error
const m: Mounts<T.SDKManifest, never> = a

View File

@@ -1,8 +1,8 @@
import * as T from "../../../base/lib/types"
import { SubContainer, SubContainerOwned } from "../util/SubContainer"
import { CommandController } from "./CommandController"
import { Daemon } from "./Daemon"
import { DaemonCommandType } from "./Daemons"
import * as T from '../../../base/lib/types'
import { SubContainer, SubContainerOwned } from '../util/SubContainer'
import { CommandController } from './CommandController'
import { Daemon } from './Daemon'
import { DaemonCommandType } from './Daemons'
/**
* This is a wrapper around CommandController that has a state of off, where the command shouldn't be running

View File

@@ -1,7 +1,7 @@
import * as T from "../../../base/lib/types"
import { Daemons } from "./Daemons"
import "../../../base/lib/interfaces/ServiceInterfaceBuilder"
import "../../../base/lib/interfaces/Origin"
import * as T from '../../../base/lib/types'
import { Daemons } from './Daemons'
import '../../../base/lib/interfaces/ServiceInterfaceBuilder'
import '../../../base/lib/interfaces/Origin'
export const DEFAULT_SIGTERM_TIMEOUT = 60_000
/**