@davidvornholt/standards 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,9 +33,9 @@ standards poller --print-units --config <path>
33
33
 
34
34
  `structure` enforces the canonical monorepo structure contract: workspace scripts (`check-types`, `lint`, `lint:fix`, `test`), root gate scripts, filtered Turbo convenience aliases, internal `0.0.0`/`workspace:*` versioning, package `exports`, shared tsconfig inheritance, and browser a11y wiring. Only a workspace containing an explicit `*.a11y.ts` suite must provide a `test:a11y` script and direct `@axe-core/playwright` and `@playwright/test` dependencies. `check` includes `structure`, so these rules gate every consumer PR.
35
35
 
36
- `poller` runs one tick of the fix poller: host-level automation that turns maintainer-approved GitHub issues into verified draft PRs and maintainer-approved draft PRs into completed review-fix cycles, using headless Codex runs. All workflow state lives in GitHub labels, comments, and draft PRs (the labels are canonical declared settings — see `github --apply`), so ticks are stateless, re-runnable, and safe to schedule from any host. A tick claims work by label (`fix-in-progress` / `review-in-progress`), releases claims stale for longer than `staleClaimHours`, and fails loudly — a red tick means jobs stalled, never silence. Trust is verified per event, not per configuration: the approval labels (`approved-for-fix`, `approved-for-review`) only count when the applying user holds the `admin` or `maintain` role, bind to the exact approved issue revision or PR head, and are revalidated before publication; only admin/maintain comments are treated as answers to agent questions. The poller removes its direct GitHub token variables before launching Codex, but the approved run shares the host service identity and can read its other ambient credentials and logged-in tool state; that accepted visibility must not be treated as credential isolation. The run works in a throwaway git worktree, hands results back through a structured outcome file, and the poller verifies effects (commits exist, no protected paths touched) before pushing, opening the draft PR, or posting the review — the required CI checks on the resulting PR remain the quality gate. Host mutation is declarative-only: `--print-units` emits a correctly escaped systemd service and timer for the polling host's infrastructure repository to adapt and deploy. That repository owns the service identity, writable HOME, PATH, GitHub token environment, lingering, and deployment. The service's tick budget (`TimeoutStartSec`) is derived from `maxJobsPerTick` × `runTimeoutMinutes` plus overhead, so re-render and redeploy the units after changing either value.
36
+ `poller` runs one tick of the fix poller: host-level automation that turns maintainer-approved GitHub issues into verified draft PRs and maintainer-approved draft PRs into completed review-fix cycles, using headless Codex runs. All workflow state lives in GitHub labels, comments, and draft PRs (the labels are canonical declared settings — see `github --apply`), so ticks are stateless, re-runnable, and safe to schedule from any host. A tick claims work by label (`fix-in-progress` / `review-in-progress`), releases claims stale for longer than `staleClaimHours`, and fails loudly — a red tick means jobs stalled, never silence. Trust is verified per event, not per configuration: the approval labels (`approved-for-fix`, `approved-for-review`) only count when the applying user holds the `admin` or `maintain` role, bind to the exact approved issue revision or PR head, and are revalidated before publication; only admin/maintain comments are treated as answers to agent questions. The poller removes its direct GitHub token variables before launching Codex, but the approved run shares the host service identity and can read its other ambient credentials and logged-in tool state; that accepted visibility must not be treated as credential isolation. The run works in a throwaway git worktree, grants Codex write access to that worktree and its exact Git common directory so commits can update the linked repository metadata, hands results back through a structured outcome file, and then verifies effects (commits exist, no protected paths touched) before pushing, opening the draft PR, or posting the review — the required CI checks on the resulting PR remain the quality gate. Host mutation is declarative-only: `--print-units` emits a correctly escaped systemd service and timer for the polling host's infrastructure repository to adapt and deploy. That repository owns the service identity, writable HOME, PATH, GitHub token environment, lingering, and deployment. The service's tick budget (`TimeoutStartSec`) is derived from `maxJobsPerTick` × `runTimeoutMinutes` plus overhead, so re-render and redeploy the units after changing either value.
37
37
 
38
- `creds` is the credential broker: it mints scoped Cloudflare tokens, rotates and revokes those tokens, and places GitHub App credentials into SOPS-encrypted targets — secret values never touch stdout, argv, or an agent's context; commands print only metadata (token name, scopes, expiry, destination). Cloudflare reconciliation has exactly two sources of truth: the plaintext key structure of the SOPS files and the provider token list under the deterministic naming scheme `standards/<owner>/<repo>/<sops-target>/<dotted.key>`; there is no separate credential manifest. `plan` and `apply` reconcile Cloudflare only: each run uses the stored bootstrap credential to verify its current provider token ID, excludes that ID from reconciliation regardless of its current name, and aborts if the identity cannot be correlated with the complete account token list; a brokered token whose SOPS key was deleted is revoked, while one nearing expiry (30 days) is renewed by creating a fresh-expiry replacement with the live token's policy, durably writing and verifying the new value, and only then revoking the old token; secrets the broker did not mint are never touched. Without a manifest there is no desired-policy record, so live policy drift is not detected, and a repository rename or transfer changes the deterministic namespace and requires the visible old token to be re-minted under the new name and revoked. `add cloudflare` mints an account-owned API token (default TTL 90 days) scoped to the named permission groups (`permissions` lists the valid names) and writes it to `--dest <target>:<dotted.key>`, where `<target>` resolves like the canonical `just secrets` targets (`ci` → `secrets/ci.yaml`, `prod-1` → `infra/hosts/prod-1/secrets.yaml`); zone-scoped groups such as DNS Write are rejected because the current command supplies an account resource, not a zone resource. `add github` writes the broker GitHub App's `app_id` and `private_key` under the destination key; durable GitHub tokens cannot be created via API (PAT and OAuth-app creation are UI-only), so workflows mint short-lived installation tokens from the App at runtime (`actions/create-github-app-token`), scoped per repository and permission. Bootstrap is once per machine: `login github` runs the App manifest flow (a localhost page posts the pre-filled manifest; one "Create GitHub App" click; credentials return through the conversion endpoint — nothing is copied by hand) and opens the installation page, where the App must be bound to selected repositories rather than all repositories; `login cloudflare` guides the one dashboard paste Cloudflare requires for the first account token (named `standards-broker` to match the App, created from the "Create additional tokens" token template because its "Account API Tokens: Edit" permission is unavailable in the custom token builder, and holding that permission only), verifies that it is active, correlates its verified ID with the complete account token list and an active list record, and stores it; a bootstrap token named inside the minted `standards/` namespace is rejected before storage to keep its role unambiguous, while any other divergence from the recommended name only warns. `plan` and `apply` do not rotate GitHub App keys; create a replacement key on the App's settings page, atomically replace `github.private_key` in the protected machine-global broker store while preserving its `0600` mode, then update every intended SOPS destination, verify workflows can mint installation tokens, and revoke the old key manually. Root credentials live in the machine-global broker store (see Configuration); SOPS writes go through `sops edit` with a non-interactive editor, preserving comments and reusing the file's data key.
38
+ `creds` is the credential broker: it mints scoped Cloudflare tokens, rotates and revokes those tokens, and places GitHub App credentials into SOPS-encrypted targets — secret values never touch stdout, argv, or an agent's context; commands print only metadata (token name, scopes, expiry, destination). Cloudflare reconciliation has exactly two sources of truth: the plaintext key structure of the SOPS files and the provider token list under the deterministic naming scheme `standards/<owner>/<repo>/<sops-target>/<dotted.key>`; there is no separate credential manifest. `plan` and `apply` reconcile Cloudflare only: each run uses the stored bootstrap credential to verify its current provider token ID, excludes that ID from reconciliation regardless of its current name, and aborts if the identity cannot be correlated with the complete account token list; a brokered token whose SOPS key was deleted is revoked, while one nearing expiry (30 days) is renewed by creating a fresh-expiry replacement with the live token's policy, durably writing and verifying the new value, and only then revoking the old token; secrets the broker did not mint are never touched. Without a manifest there is no desired-policy record, so live policy drift is not detected, and a repository rename or transfer changes the deterministic namespace and requires the visible old token to be re-minted under the new name and revoked. `add cloudflare` mints an account-owned API token (default TTL 90 days) scoped to the named permission groups (`permissions` lists the valid names) and writes it to `--dest <target>:<dotted.key>`, where `<target>` resolves like the canonical `just secrets` targets (`ci` → `secrets/ci.yaml`, `prod-1` → `infra/hosts/prod-1/secrets.yaml`); zone-scoped groups such as DNS Write are rejected because the current command supplies an account resource, not a zone resource. `add github` writes the broker GitHub App's `app_id` and `private_key` under the destination key; durable GitHub tokens cannot be created via API (PAT and OAuth-app creation are UI-only), so workflows mint short-lived installation tokens from the App at runtime (`actions/create-github-app-token`), scoped per repository and permission. The canonical weekly sync expects `bun standards creds add github --dest ci:ci.broker_app`, then mints a current-repository token with Contents read and Pull requests write; missing credentials or minting failures stop it without a fallback. Bootstrap is once per machine: `login github` runs the App manifest flow (a localhost page posts the pre-filled manifest; one "Create GitHub App" click; credentials return through the conversion endpoint — nothing is copied by hand) and opens the installation page, where the App must be bound to selected repositories rather than all repositories; `login cloudflare` guides the one dashboard paste Cloudflare requires for the first account token (named `standards-broker` to match the App, created from the "Create additional tokens" token template because its "Account API Tokens: Edit" permission is unavailable in the custom token builder, and holding that permission only), verifies that it is active, correlates its verified ID with the complete account token list and an active list record, and stores it; a bootstrap token named inside the minted `standards/` namespace is rejected before storage to keep its role unambiguous, while any other divergence from the recommended name only warns. `plan` and `apply` do not rotate GitHub App keys; create a replacement key on the App's settings page, atomically replace `github.private_key` in the protected machine-global broker store while preserving its `0600` mode, then update every intended SOPS destination, verify workflows can mint installation tokens, and revoke the old key manually. Root credentials live in the machine-global broker store (see Configuration); SOPS writes go through `sops edit` with a non-interactive editor, preserving comments and reusing the file's data key.
39
39
 
40
40
  `structure --profile source` validates the standards source repository itself, which is deliberately not a consumer. The profile pins its intentional exceptions so they cannot drift silently: exact, ordered root gate scripts that run this CLI from the local checkout (`structure --profile source`, `dependabot --check` or `dependabot --write`, `github --check`, and the Turbo gate) instead of a recursive `standards check`, and a non-private published bin-only CLI workspace that carries a stable release SemVer and exactly maps the `standards` bin to `src/cli.ts` without `exports`. Every other consumer rule still applies.
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,6 +34,13 @@ type CodexExecutor = (
34
34
 
35
35
  const defaultExecutor: CodexExecutor = execFileSync;
36
36
 
37
+ type CodexRunOptions = {
38
+ readonly workDir: string;
39
+ readonly gitCommonDir: string;
40
+ readonly prompt: string;
41
+ readonly config: PollerConfig;
42
+ };
43
+
37
44
  const exitCause = (details: Record<string, unknown>): string => {
38
45
  if (typeof details.status === 'number') {
39
46
  return `exit status ${details.status}`;
@@ -57,11 +64,10 @@ const agentEnv = (): Record<string, string | undefined> => {
57
64
  };
58
65
 
59
66
  export const runCodex = (
60
- workDir: string,
61
- prompt: string,
62
- config: PollerConfig,
67
+ options: CodexRunOptions,
63
68
  execute: CodexExecutor = defaultExecutor,
64
69
  ): CodexRunResult => {
70
+ const { workDir, gitCommonDir, prompt, config } = options;
65
71
  rmSync(join(workDir, OUTCOME_DIR), { recursive: true, force: true });
66
72
  try {
67
73
  execute(
@@ -72,6 +78,8 @@ export const runCodex = (
72
78
  workDir,
73
79
  '--sandbox',
74
80
  'workspace-write',
81
+ '--add-dir',
82
+ gitCommonDir,
75
83
  '-c',
76
84
  'sandbox_workspace_write.network_access=true',
77
85
  '-m',
@@ -139,9 +139,10 @@ export const runFixJob = async (
139
139
  workspace,
140
140
  };
141
141
  try {
142
- const run = runCodex(
143
- workspace.dir,
144
- fixPrompt({
142
+ const run = runCodex({
143
+ workDir: workspace.dir,
144
+ gitCommonDir: cacheClone,
145
+ prompt: fixPrompt({
145
146
  repo,
146
147
  issueNumber: issue.number,
147
148
  title: currentIssue.title,
@@ -149,7 +150,7 @@ export const runFixJob = async (
149
150
  answers: preamble.answers,
150
151
  }),
151
152
  config,
152
- );
153
+ });
153
154
  await validateFixClaim(job);
154
155
  const outcome = run.succeeded ? await readFixOutcome(workspace.dir) : null;
155
156
  if (outcome === null) {
@@ -36,17 +36,18 @@ export const executeReviewJob = async (options: {
36
36
  ),
37
37
  );
38
38
  try {
39
- const run = runCodex(
40
- workspace.dir,
41
- reviewPrompt({
39
+ const run = runCodex({
40
+ workDir: workspace.dir,
41
+ gitCommonDir: cacheClone,
42
+ prompt: reviewPrompt({
42
43
  repo: deps.repo,
43
44
  prNumber: pr.number,
44
45
  title: pr.title,
45
46
  baseSha: reviewBase,
46
47
  answers,
47
48
  }),
48
- deps.config,
49
- );
49
+ config: deps.config,
50
+ });
50
51
  await validateReviewClaim({
51
52
  deps,
52
53
  pr,