From 057aef0fc8e715952c394af4be8ba3fc6ba59b1d Mon Sep 17 00:00:00 2001
From: satyaborg <satya.borg@gmail.com>
Date: Thu, 28 May 2026 21:38:25 +1000
Subject: [PATCH 1/2] feat: npm-release-readiness

---
 .github/ISSUE_TEMPLATE/bug_report.yml      |  36 +++++
 .github/ISSUE_TEMPLATE/feature_request.yml |  29 ++++
 .github/PULL_REQUEST_TEMPLATE.md           |  13 ++
 .github/workflows/ci.yml                   |  30 ++++
 .github/workflows/publish.yml              |  55 +++++++
 .github/workflows/release.yml              |  21 +++
 .release-please-manifest.json              |   3 +
 CHANGELOG.md                               |   5 +
 CODE_OF_CONDUCT.md                         |   9 ++
 CONTRIBUTING.md                            |  53 ++++++
 README.md                                  |  45 +++++-
 SECURITY.md                                |  21 +++
 bun.lock                                   |   2 +-
 package.json                               |  39 ++++-
 release-please-config.json                 |  11 ++
 scripts/package-smoke.ts                   |  88 ++++++++++
 tests/package.test.ts                      | 177 +++++++++++++++++++++
 17 files changed, 630 insertions(+), 7 deletions(-)
 create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml
 create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml
 create mode 100644 .github/PULL_REQUEST_TEMPLATE.md
 create mode 100644 .github/workflows/ci.yml
 create mode 100644 .github/workflows/publish.yml
 create mode 100644 .github/workflows/release.yml
 create mode 100644 .release-please-manifest.json
 create mode 100644 CHANGELOG.md
 create mode 100644 CODE_OF_CONDUCT.md
 create mode 100644 CONTRIBUTING.md
 create mode 100644 SECURITY.md
 create mode 100644 release-please-config.json
 create mode 100644 scripts/package-smoke.ts
 create mode 100644 tests/package.test.ts

diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..fdde8dd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,36 @@
+name: Bug report
+description: Report a reproducible devloop bug
+title: "fix: "
+labels: ["bug"]
+body:
+  - type: textarea
+    id: summary
+    attributes:
+      label: Summary
+      description: What failed, and what did you expect instead?
+    validations:
+      required: true
+  - type: textarea
+    id: reproduce
+    attributes:
+      label: Reproduction
+      description: Include the exact devloop command, spec shape, and relevant output.
+      placeholder: |
+        devloop --plain .specs/change.md
+    validations:
+      required: true
+  - type: input
+    id: version
+    attributes:
+      label: devloop version
+      placeholder: "@satyaborg/devloop 0.1.0"
+  - type: textarea
+    id: environment
+    attributes:
+      label: Environment
+      description: Bun version, OS, shell, and configured coder/reviewer agents.
+  - type: textarea
+    id: logs
+    attributes:
+      label: Logs
+      description: Paste only logs that are safe to share. `.codex/` artifacts can contain prompts and local paths.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..73414f8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,29 @@
+name: Feature request
+description: Propose a focused devloop improvement
+title: "feat: "
+labels: ["enhancement"]
+body:
+  - type: textarea
+    id: problem
+    attributes:
+      label: Problem
+      description: What workflow is hard or missing today?
+    validations:
+      required: true
+  - type: textarea
+    id: proposal
+    attributes:
+      label: Proposal
+      description: What should devloop do differently?
+    validations:
+      required: true
+  - type: textarea
+    id: acceptance
+    attributes:
+      label: Acceptance criteria
+      description: List concrete checks that would prove the feature works.
+  - type: textarea
+    id: scope
+    attributes:
+      label: Scope notes
+      description: What should stay out of scope?
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..2303259
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,13 @@
+## Summary
+
+## Implementation
+
+## Verification
+
+- [ ] `bun run typecheck`
+- [ ] `bun test`
+- [ ] `bun run package:smoke` when packaging, install docs, or release automation changes
+
+## Notes
+
+- [ ] No `.codex/`, `.specs/`, coverage output, dependency caches, or local worktrees included
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..034874f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,30 @@
+name: CI
+
+on:
+  pull_request:
+  push:
+    branches: [main]
+
+permissions:
+  contents: read
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    env:
+      NPM_CONFIG_CACHE: ${{ runner.temp }}/devloop-npm-cache
+    steps:
+      - uses: actions/checkout@v6
+      - uses: oven-sh/setup-bun@v2
+        with:
+          bun-version: latest
+      - uses: actions/setup-node@v6
+        with:
+          node-version: 24
+          registry-url: https://registry.npmjs.org
+          package-manager-cache: false
+      - run: bun install --frozen-lockfile
+      - run: bun run typecheck
+      - run: bun test
+      - run: npm --cache "$NPM_CONFIG_CACHE" pack --dry-run --json
+      - run: bun run package:smoke
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..d485257
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,55 @@
+name: Publish
+
+on:
+  workflow_run:
+    workflows: ["Release Please"]
+    types: [completed]
+
+permissions:
+  contents: read
+  id-token: write
+
+jobs:
+  npm:
+    if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main'
+    runs-on: ubuntu-latest
+    env:
+      NPM_CONFIG_CACHE: ${{ runner.temp }}/devloop-npm-cache
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          ref: main
+      - uses: oven-sh/setup-bun@v2
+        with:
+          bun-version: latest
+      - uses: actions/setup-node@v6
+        with:
+          node-version: 24
+          registry-url: https://registry.npmjs.org
+          package-manager-cache: false
+      - run: bun install --frozen-lockfile
+      - id: publishable
+        env:
+          GH_TOKEN: ${{ github.token }}
+        run: |
+          VERSION="$(node -p "require('./package.json').version")"
+          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+          if ! gh release view "v$VERSION" >/dev/null 2>&1; then
+            echo "publish=false" >> "$GITHUB_OUTPUT"
+            exit 0
+          fi
+          if npm --cache "$NPM_CONFIG_CACHE" view "@satyaborg/devloop@$VERSION" version >/dev/null 2>&1; then
+            echo "publish=false" >> "$GITHUB_OUTPUT"
+            exit 0
+          fi
+          echo "publish=true" >> "$GITHUB_OUTPUT"
+      - if: steps.publishable.outputs.publish == 'true'
+        run: bun run typecheck
+      - if: steps.publishable.outputs.publish == 'true'
+        run: bun test
+      - if: steps.publishable.outputs.publish == 'true'
+        run: npm --cache "$NPM_CONFIG_CACHE" pack --dry-run --json
+      - if: steps.publishable.outputs.publish == 'true'
+        run: bun run package:smoke
+      - if: steps.publishable.outputs.publish == 'true'
+        run: npm publish
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..c1d4c9d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,21 @@
+name: Release Please
+
+on:
+  push:
+    branches: [main]
+
+permissions:
+  contents: write
+  pull-requests: write
+  issues: write
+
+jobs:
+  release-please:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: googleapis/release-please-action@v4
+        with:
+          token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }}
+          config-file: release-please-config.json
+          manifest-file: .release-please-manifest.json
+          # release-please-config.json sets release-type: node and CHANGELOG.md.
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..466df71
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+  ".": "0.1.0"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..b57427b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+Release Please updates this file from Conventional Commits when release PRs are opened.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..b6867e1
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,9 @@
+# Code Of Conduct
+
+Be direct, respectful, and specific.
+
+This project is for building reliable local agent workflows. Contributions should focus on the work, avoid personal attacks, and make technical disagreements easy to evaluate.
+
+Unacceptable behavior includes harassment, threats, doxxing, sustained disruption, or comments that make participation hostile for others.
+
+Report conduct issues through GitHub issues or security advisories when privacy is needed. Maintainers may remove comments, close issues, or block participants to keep the project usable.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..43d7090
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,53 @@
+# Contributing
+
+devloop is a Bun and TypeScript CLI. Keep changes narrow, typed, and covered by tests.
+
+## Local Setup
+
+```sh
+bun scripts/install.ts
+bun run typecheck
+bun test
+bun run package:smoke
+```
+
+`bun test` is configured with 100% line, function, and statement coverage for non-test TypeScript files.
+
+## Pull Requests
+
+- Use Conventional Commits in commit subjects: `feat:`, `fix:`, or `chore:`. Use `!` for breaking changes.
+- Keep one logical change per commit.
+- Include test evidence in the PR description.
+- Update README examples when user-visible CLI behavior changes.
+- Do not commit `.codex/`, `.specs/`, coverage output, dependency caches, or local worktrees.
+
+## Release Automation
+
+Release Please manages version bumps from Conventional Commits, opens release PRs, updates `CHANGELOG.md`, and creates GitHub releases when release PRs merge. The repository uses `release-please-config.json` with the `node` release type for `@satyaborg/devloop`.
+
+The release workflow can use the default GitHub token, but GitHub-token-created pull requests and releases can have workflow trigger limitations. Maintainers who need CI to run on Release Please PRs should create a fine-grained `RELEASE_PLEASE_TOKEN` with repository contents and pull request permissions, then store it as a GitHub Actions secret. The workflow falls back to the default token when that secret is absent.
+
+## npm Publishing
+
+The package is published as `@satyaborg/devloop`; the installed binary remains `devloop`.
+
+Publishing is handled by `.github/workflows/publish.yml`. It runs after the Release Please workflow completes, verifies that a matching GitHub release exists for the package version, skips versions that are already on npm, reruns typecheck/tests/package checks, and then runs `npm publish`.
+
+The publish workflow uses npm trusted publishing through GitHub Actions OIDC. It intentionally does not use a long-lived `NPM_TOKEN` or `NODE_AUTH_TOKEN`.
+
+One-time npm setup before the first automated publish:
+
+1. Ensure the `@satyaborg` npm scope is owned by the maintainer account.
+2. `@satyaborg/devloop` must be created or first-published by a maintainer as a public package if npm requires an existing package before trusted publishing can be configured.
+3. In npm package settings, add a trusted publisher for GitHub Actions.
+4. Configure owner `satyaborg`, repository `devloop`, workflow filename `publish.yml`, and allowed action `npm publish`.
+5. Leave the environment blank unless the GitHub workflow is later changed to use a protected environment.
+
+For a manual first publish, verify locally first:
+
+```sh
+bun run typecheck
+bun test
+npm --cache /private/tmp/devloop-npm-cache pack --dry-run --json
+bun run package:smoke
+```
diff --git a/README.md b/README.md
index 1360622..1e8755a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,8 @@
+[![CI](https://github.com/satyaborg/devloop/actions/workflows/ci.yml/badge.svg)](https://github.com/satyaborg/devloop/actions/workflows/ci.yml)
+[![npm version](https://img.shields.io/npm/v/@satyaborg/devloop.svg)](https://www.npmjs.com/package/@satyaborg/devloop)
+[![license](https://img.shields.io/npm/l/@satyaborg/devloop.svg)](LICENSE)
+[![npm downloads](https://img.shields.io/npm/dm/@satyaborg/devloop.svg)](https://www.npmjs.com/package/@satyaborg/devloop)
+
 ```text
        __          __
   ____/ /__ _   __/ /___  ____  ____
@@ -13,15 +18,36 @@ The coder makes the change. The reviewer reviews it. If the reviewer rejects it,
 
 ## Install
 
-From this checkout:
+Prerequisites:
+
+- Bun 1.2 or newer
+- git
+- the local agent CLIs you configure
+- Codex and Claude Code for the default coder/reviewer pairing
+
+Install the public npm package globally:
 
 ```sh
-bun scripts/install.ts
+npm install -g @satyaborg/devloop
 ```
 
-This installs dependencies and links `devloop` into `~/.local/bin`.
+Run without a global install:
 
-To use another bin directory:
+```sh
+bunx @satyaborg/devloop --help
+```
+
+The npm package name is `@satyaborg/devloop`, and the binary remains `devloop`.
+
+For source checkout development:
+
+```sh
+git clone https://github.com/satyaborg/devloop.git
+cd devloop
+bun scripts/install.ts
+```
+
+This installs dependencies and links `devloop` into `~/.local/bin`. To use another bin directory:
 
 ```sh
 DEVLOOP_BIN_DIR=/path/to/bin bun scripts/install.ts
@@ -100,7 +126,7 @@ By default, `devloop`:
 - writes an HTML report
 - requires the reviewer to pass every acceptance criterion with implementation and test evidence
 - asks the reviewer to flag silent decisions, scope drift, and missing tests
-- creates an isolated sibling git worktree and runs agents there
+- creates isolated sibling git worktrees by default and runs agents there
 - creates one conventional commit after each coder pass, before review
 - never pushes or opens a PR
 
@@ -161,6 +187,14 @@ git -C <source-repo> branch -D 'feat!/<slug>'
 
 If `worktree remove` reports local modifications, inspect the worktree first or rerun the command with `--force` to discard them.
 
+## Security Model
+
+devloop runs local agent CLIs with broad permissions because the configured coder and reviewer need to inspect and change a checkout. By default it creates isolated sibling git worktrees before running those agents, but the agents still execute on your machine with your local credentials and PATH.
+
+devloop writes `.codex/` artifacts for specs, tracks, reviews, reports, logs, and session ids. devloop does not add telemetry, phone home, or send data anywhere itself. Network access depends on the agent CLIs and commands you configure.
+
+CI currently verifies the package on Ubuntu. The maintainer development environment is macOS with Bun; other platforms may work but are not release-gated yet.
+
 ## Development
 
 Prereqs: `bun`, `git`, and the agents you configure. The defaults require `codex` and `claude`.
@@ -169,6 +203,7 @@ Prereqs: `bun`, `git`, and the agents you configure. The defaults require `codex
 bun scripts/install.ts
 bun run typecheck
 bun test
+bun run package:smoke
 ```
 
 `bun test` enforces 100% line, function, and statement coverage for the TypeScript core.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..8a611d9
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security
+
+## Security Model
+
+devloop runs local agent CLIs with broad permissions because the configured coder and reviewer need access to inspect and modify a checkout. By default, devloop creates isolated sibling git worktrees before invoking those agents, but the agents still run on your machine with your local credentials, environment variables, PATH, and filesystem permissions.
+
+devloop writes `.codex/` artifacts for specs, tracks, reviews, reports, logs, and session ids. Treat those files as local development artifacts that may contain prompts, review text, command output, and repository paths.
+
+devloop does not add telemetry, phone home, or send data anywhere itself. Network access depends on the agent CLIs and commands you configure.
+
+## Reporting A Vulnerability
+
+Open a private security advisory at:
+
+https://github.com/satyaborg/devloop/security/advisories/new
+
+If that is not available, open a minimal issue that does not include exploit details and ask for a private contact path.
+
+## Supported Versions
+
+Security fixes target the latest released npm package and the `main` branch.
diff --git a/bun.lock b/bun.lock
index 9c82dff..0facbb4 100644
--- a/bun.lock
+++ b/bun.lock
@@ -3,7 +3,7 @@
   "configVersion": 1,
   "workspaces": {
     "": {
-      "name": "devloop",
+      "name": "@satyaborg/devloop",
       "dependencies": {
         "@opentui/core": "^0.2.15",
       },
diff --git a/package.json b/package.json
index 2c2799f..7f6ea4c 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,49 @@
 {
-  "name": "devloop",
+  "name": "@satyaborg/devloop",
   "version": "0.1.0",
+  "description": "Codex and Claude Code implementation review loop CLI.",
+  "license": "MIT",
+  "author": {
+    "name": "Satya Borg",
+    "url": "https://satyaborg.com"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/satyaborg/devloop.git"
+  },
+  "bugs": {
+    "url": "https://github.com/satyaborg/devloop/issues"
+  },
+  "homepage": "https://github.com/satyaborg/devloop#readme",
+  "keywords": [
+    "agent",
+    "automation",
+    "claude",
+    "cli",
+    "codex",
+    "devloop"
+  ],
   "type": "module",
   "bin": {
     "devloop": "./src/cli.ts"
   },
+  "files": [
+    "src",
+    "skills/spec/SKILL.md",
+    "templates/spec.md",
+    "README.md",
+    "LICENSE"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "engines": {
+    "bun": ">=1.2.0"
+  },
+  "packageManager": "bun@1.3.11",
   "scripts": {
     "install:local": "bun scripts/install.ts",
+    "package:smoke": "bun scripts/package-smoke.ts",
     "test": "bun test",
     "typecheck": "tsc --noEmit"
   },
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..94d3c09
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,11 @@
+{
+  "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
+  "packages": {
+    ".": {
+      "release-type": "node",
+      "package-name": "@satyaborg/devloop",
+      "changelog-path": "CHANGELOG.md",
+      "include-component-in-tag": false
+    }
+  }
+}
diff --git a/scripts/package-smoke.ts b/scripts/package-smoke.ts
new file mode 100644
index 0000000..189030c
--- /dev/null
+++ b/scripts/package-smoke.ts
@@ -0,0 +1,88 @@
+#!/usr/bin/env bun
+import { mkdir, mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+type PackFile = { path: string };
+type PackResult = {
+  filename: string;
+  files: PackFile[];
+  name: string;
+  version: string;
+};
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const work = await mkdtemp(path.join(tmpdir(), "devloop-package-smoke."));
+const cache = path.join(work, "npm-cache");
+const packDir = path.join(work, "pack");
+const prefix = path.join(work, "prefix");
+
+try {
+  await mkdir(packDir, { recursive: true });
+  const output = await run(["npm", "--cache", cache, "pack", "--json", "--pack-destination", packDir], root);
+  const [pack] = JSON.parse(output) as PackResult[];
+  if (!pack) throw new Error("npm pack did not return package metadata");
+
+  const paths = pack.files.map((file) => file.path).sort();
+  for (const required of [
+    "package.json",
+    "README.md",
+    "LICENSE",
+    "src/cli.ts",
+    "src/devloop.ts",
+    "src/spec.ts",
+    "src/tui.ts",
+    "src/tui-view.ts",
+    "skills/spec/SKILL.md",
+    "templates/spec.md",
+  ]) {
+    if (!paths.includes(required)) throw new Error(`packed package is missing ${required}`);
+  }
+
+  for (const excluded of [
+    "AGENTS.md",
+    "bunfig.toml",
+    "tsconfig.json",
+    "scripts/install.ts",
+  ]) {
+    if (paths.includes(excluded)) throw new Error(`packed package includes ${excluded}`);
+  }
+
+  for (const excludedPrefix of ["tests/", "coverage/", ".codex/", ".specs/", ".github/"]) {
+    const match = paths.find((item) => item.startsWith(excludedPrefix));
+    if (match) throw new Error(`packed package includes ${match}`);
+  }
+
+  const tarball = path.join(packDir, pack.filename);
+  await run(["npm", "--cache", cache, "install", "--global", tarball, "--prefix", prefix, "--no-audit", "--fund=false"], root);
+  const help = await run([installedBin(prefix), "--help"], root);
+  if (!help.includes("Common commands:")) throw new Error("installed devloop --help did not print CLI help");
+
+  console.log(`package smoke passed: ${pack.name}@${pack.version}`);
+} finally {
+  await rm(work, { recursive: true, force: true });
+}
+
+async function run(cmd: string[], cwd: string) {
+  const proc = Bun.spawn(cmd, {
+    cwd,
+    env: Bun.env,
+    stdout: "pipe",
+    stderr: "pipe",
+  });
+  const [stdout, stderr, code] = await Promise.all([
+    new Response(proc.stdout).text(),
+    new Response(proc.stderr).text(),
+    proc.exited,
+  ]);
+  if (code !== 0)
+    throw new Error(`${cmd.join(" ")} failed with ${code}\n${stdout}${stderr}`.trim());
+  return stdout;
+}
+
+function installedBin(prefixDir: string) {
+  return process.platform === "win32"
+    ? path.join(prefixDir, "devloop.cmd")
+    : path.join(prefixDir, "bin", "devloop");
+}
diff --git a/tests/package.test.ts b/tests/package.test.ts
new file mode 100644
index 0000000..df99902
--- /dev/null
+++ b/tests/package.test.ts
@@ -0,0 +1,177 @@
+import { afterAll, describe, expect, test } from "bun:test";
+import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import path from "node:path";
+
+const root = path.resolve(import.meta.dir, "..");
+const npmCache = await mkdtemp(path.join(tmpdir(), "devloop-npm-cache."));
+
+afterAll(async () => rm(npmCache, { recursive: true, force: true }));
+
+describe("npm package readiness", () => {
+  test("declares public package metadata for the scoped npm package", async () => {
+    const pkg = await packageJson();
+
+    expect(pkg.name).toBe("@satyaborg/devloop");
+    expect(pkg.bin).toEqual({ devloop: "./src/cli.ts" });
+    expect(pkg.description).toBe("Codex and Claude Code implementation review loop CLI.");
+    expect(pkg.license).toBe("MIT");
+    expect(pkg.author).toEqual({ name: "Satya Borg", url: "https://satyaborg.com" });
+    expect(pkg.repository).toEqual({ type: "git", url: "git+https://github.com/satyaborg/devloop.git" });
+    expect(pkg.bugs).toEqual({ url: "https://github.com/satyaborg/devloop/issues" });
+    expect(pkg.homepage).toBe("https://github.com/satyaborg/devloop#readme");
+    expect(pkg.keywords).toEqual(expect.arrayContaining(["agent", "codex", "claude", "cli", "devloop"]));
+    expect(pkg.packageManager).toMatch(/^bun@\d+\.\d+\.\d+$/);
+    expect(pkg.engines).toEqual({ bun: ">=1.2.0" });
+    expect(pkg.publishConfig).toEqual({ access: "public" });
+  });
+
+  test("allows only runtime package files into the packed tarball", async () => {
+    const pkg = await packageJson();
+
+    expect(pkg.files).toEqual([
+      "src",
+      "skills/spec/SKILL.md",
+      "templates/spec.md",
+      "README.md",
+      "LICENSE",
+    ]);
+
+    const paths = await dryRunPackFiles();
+    for (const required of [
+      "package.json",
+      "README.md",
+      "LICENSE",
+      "src/cli.ts",
+      "src/devloop.ts",
+      "src/spec.ts",
+      "src/tui.ts",
+      "src/tui-view.ts",
+      "skills/spec/SKILL.md",
+      "templates/spec.md",
+    ]) {
+      expect(paths).toContain(required);
+    }
+
+    for (const forbidden of [
+      "AGENTS.md",
+      "bunfig.toml",
+      "tsconfig.json",
+      "scripts/install.ts",
+    ]) {
+      expect(paths).not.toContain(forbidden);
+    }
+
+    for (const prefix of ["tests/", "coverage/", ".codex/", ".specs/", ".github/"]) {
+      expect(paths.some((item) => item.startsWith(prefix))).toBe(false);
+    }
+  });
+});
+
+describe("release readiness documentation", () => {
+  test("documents public install paths, badges, prerequisites, and security model", async () => {
+    const readme = await readFile(path.join(root, "README.md"), "utf8");
+
+    expect(readme).toContain("[![CI](https://github.com/satyaborg/devloop/actions/workflows/ci.yml/badge.svg)]");
+    expect(readme).toContain("[![npm version](https://img.shields.io/npm/v/@satyaborg/devloop.svg)]");
+    expect(readme).toContain("[![license](https://img.shields.io/npm/l/@satyaborg/devloop.svg)]");
+    expect(readme).toContain("[![npm downloads](https://img.shields.io/npm/dm/@satyaborg/devloop.svg)]");
+    expect(readme).toContain("npm install -g @satyaborg/devloop");
+    expect(readme).toContain("bunx @satyaborg/devloop");
+    expect(readme).toContain("the binary remains `devloop`");
+    expect(readme).toContain("Prerequisites");
+    expect(readme).toContain("Bun");
+    expect(readme).toContain("bun scripts/install.ts");
+    expect(readme).toContain("runs local agent CLIs with broad permissions");
+    expect(readme).toContain("creates isolated sibling git worktrees by default");
+    expect(readme).toContain("writes `.codex/` artifacts");
+    expect(readme).toContain("does not add telemetry");
+  });
+
+  test("documents release automation and first publish setup", async () => {
+    const contributing = await readFile(path.join(root, "CONTRIBUTING.md"), "utf8");
+    const security = await readFile(path.join(root, "SECURITY.md"), "utf8");
+
+    expect(contributing).toContain("Conventional Commits");
+    expect(contributing).toContain("Release Please");
+    expect(contributing).toContain("CHANGELOG.md");
+    expect(contributing).toContain("GitHub releases");
+    expect(contributing).toContain("trusted publishing");
+    expect(contributing).toContain("@satyaborg/devloop");
+    expect(contributing).toContain("must be created or first-published by a maintainer");
+    expect(contributing).toContain("publish.yml");
+    expect(security).toContain("runs local agent CLIs with broad permissions");
+    expect(security).toContain("does not add telemetry");
+  });
+});
+
+describe("open source project files", () => {
+  test("includes contributor, issue, pull request, CI, release, and publish files", async () => {
+    for (const file of [
+      "CONTRIBUTING.md",
+      "SECURITY.md",
+      "CODE_OF_CONDUCT.md",
+      ".github/PULL_REQUEST_TEMPLATE.md",
+      ".github/ISSUE_TEMPLATE/bug_report.yml",
+      ".github/ISSUE_TEMPLATE/feature_request.yml",
+      ".github/workflows/ci.yml",
+      ".github/workflows/release.yml",
+      ".github/workflows/publish.yml",
+      "release-please-config.json",
+      ".release-please-manifest.json",
+      "CHANGELOG.md",
+    ]) {
+      expect(await exists(path.join(root, file))).toBe(true);
+    }
+  });
+
+  test("runs CI, release, and publish workflows with the expected gates", async () => {
+    const ci = await readFile(path.join(root, ".github/workflows/ci.yml"), "utf8");
+    const release = await readFile(path.join(root, ".github/workflows/release.yml"), "utf8");
+    const publish = await readFile(path.join(root, ".github/workflows/publish.yml"), "utf8");
+
+    expect(ci).toContain("pull_request:");
+    expect(ci).toContain("push:");
+    expect(ci).toContain("branches: [main]");
+    expect(ci).toContain("oven-sh/setup-bun");
+    expect(ci).toContain("bun install --frozen-lockfile");
+    expect(ci).toContain("bun run typecheck");
+    expect(ci).toContain("bun test");
+    expect(ci).toContain("npm --cache");
+    expect(ci).toContain("pack --dry-run --json");
+    expect(ci).toContain("bun run package:smoke");
+
+    expect(release).toContain("googleapis/release-please-action");
+    expect(release).toContain("release-type");
+    expect(release).toContain("node");
+    expect(release).toContain("CHANGELOG.md");
+
+    expect(publish).toContain("workflow_run:");
+    expect(publish).toContain("Release Please");
+    expect(publish).toContain("id-token: write");
+    expect(publish).toContain("bun run typecheck");
+    expect(publish).toContain("bun test");
+    expect(publish).toContain("bun run package:smoke");
+    expect(publish).toContain("npm publish");
+    expect(publish).not.toContain("NPM_TOKEN");
+    expect(publish).not.toContain("NODE_AUTH_TOKEN");
+  });
+});
+
+async function packageJson() {
+  return JSON.parse(await readFile(path.join(root, "package.json"), "utf8")) as Record<string, unknown>;
+}
+
+async function dryRunPackFiles() {
+  const output =
+    await Bun.$`npm --cache ${npmCache} pack --dry-run --json`
+      .cwd(root)
+      .quiet()
+      .text();
+  const [pack] = JSON.parse(output) as Array<{ files: Array<{ path: string }> }>;
+  return pack.files.map((file) => file.path).sort();
+}
+
+async function exists(file: string) {
+  return Boolean(await stat(file).catch(() => false));
+}

From eb3cfd8d4f4d3a6f64541f4a86399e3ccbe9c0e3 Mon Sep 17 00:00:00 2001
From: satya <satya.borg@gmail.com>
Date: Thu, 28 May 2026 21:49:32 +1000
Subject: [PATCH 2/2] chrore: update description

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 7f6ea4c..c1b1b96 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
 {
   "name": "@satyaborg/devloop",
   "version": "0.1.0",
-  "description": "Codex and Claude Code implementation review loop CLI.",
+  "description": "Spec-driven code and review loop with Codex and Claude Code.",
   "license": "MIT",
   "author": {
-    "name": "Satya Borg",
+    "name": "@satyaborg",
     "url": "https://satyaborg.com"
   },
   "repository": {
