Files
openprose__prose/.github/workflows/cli-release-check.yml
Raymond Weitekamp 9856ac5b12 feat(spec): declared ### Skills section with fail-closed compile resolution (#62)
* feat(spec): declared ### Skills section with fail-closed compile resolution

Implements the spec from issue #60. Components declare required harness
skills via a `### Skills` section (colon form, e.g. `document-skills:pdf`).
`prose compile` resolves declared skills against ./skills/, ~/.claude/skills/,
~/.codex/skills/, and ~/.agents/skills/, and fails closed with
`skill_unresolved` before forwarding to the agent harness when any are
missing.

- Spec: skills/open-prose/contract-markdown.md gains a ### Skills row in
  the Canonical Sections table and a ## Skills H2 covering colon naming,
  search order, the BYO-harness invariant, and fail-closed semantics.
- Implementation: tools/cli/src/skills/declared.ts (parser + resolver +
  directory walker + DeclaredSkillsUnresolvedError). Pure functions; no
  I/O beyond readFile / readdir / stat.
- Wiring: tools/cli/src/commands/compile.ts pre-checks declared skills
  before forwarding the compile prompt; fails closed with
  CompileValidationError when any are unresolved. Gated behind the
  existing skillPreflight option for test parity.
- Example: skills/open-prose/examples/declared-skills/ shows the
  document-skills:pdf canonical pattern.
- Tests: 19 new (18 in declared.test.ts covering parser/resolver/walker/
  error formatter; 1 in cli.test.ts asserting compile fails closed before
  the harness is invoked when a declared skill is missing).
- BYO harness: OpenProse never installs harness skills; resolution failure
  is the user's signal to install the named skill themselves.

Resolves #60.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(spec): move skill resolution into compiler program; trim spec per review

Address review feedback on #62:

- contract-markdown.md: drop the "not duplicated in frontmatter" clause —
  the rejected alternative shouldn't propagate into the spec.
- contract-markdown.md: fail-closed clause now mentions only `prose compile`;
  `prose run` enforcement is deferred per the PR description.
- compiler/index.prose.md: add a `skills_resolver` agent that owns the
  search-path order, scope aggregation, BYO invariant, and fail-closed
  semantics. Skill resolution is now a compiler/program-level
  responsibility, not a harness responsibility, so other harnesses running
  the compiler get the same behavior.
- skills/declared.ts: add a header comment pointing at the program-level
  spec; this module is the harness implementation of `skills_resolver`.
- examples/declared-skills/README.md: update wording to reference the
  compiler agent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ci): make smoke artifacts case-specific

* fix(ci): use deterministic audit policy

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 13:50:01 -04:00

287 lines
8.0 KiB
YAML

name: CLI Release Check
on:
pull_request:
paths:
- "tools/cli/**"
- ".github/workflows/cli-release-check.yml"
- ".github/workflows/cli-real-harness-smoke.yml"
- ".github/workflows/cli-skills-smoke.yml"
- ".github/workflows/openprose-smoke.yml"
- ".github/workflows/release.yml"
- ".github/scripts/openprose-smoke/**"
- "scripts/release-preflight.sh"
- "scripts/bump-version.sh"
- ".version-bump.json"
push:
branches:
- main
paths:
- "tools/cli/**"
- ".github/workflows/cli-release-check.yml"
- ".github/workflows/cli-real-harness-smoke.yml"
- ".github/workflows/cli-skills-smoke.yml"
- ".github/workflows/openprose-smoke.yml"
- ".github/workflows/release.yml"
- ".github/scripts/openprose-smoke/**"
- "scripts/release-preflight.sh"
- "scripts/bump-version.sh"
- ".version-bump.json"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: cli-release-check-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-shell-and-actions:
name: Lint shell and workflow files
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go for actionlint
uses: actions/setup-go@v5
with:
go-version: stable
cache: false
- name: Install actionlint
run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7
- name: Lint GitHub Actions workflows
run: actionlint .github/workflows/cli-*.yml .github/workflows/plugin-manifest.yml .github/workflows/openprose-smoke.yml .github/workflows/release.yml
- name: Check installer shell syntax
run: bash -n tools/cli/install.sh
- name: Check release helper syntax
run: node --check tools/cli/scripts/build-release-tarball.mjs
- name: Check harness smoke helper syntax
run: node --check tools/cli/scripts/smoke-harness.mjs
- name: Check release shell syntax
run: |
bash -n scripts/bump-version.sh
bash -n scripts/release-preflight.sh
- name: OpenProse versions in sync
run: ./scripts/bump-version.sh --check
node-checks:
name: Node ${{ matrix.node-version }} checks
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
node-version:
- 18
- 20
- 22
defaults:
run:
working-directory: tools/cli
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
cache-dependency-path: tools/cli/package-lock.json
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Build
run: npm run build
- name: Smoke built CLI
run: |
set -euo pipefail
node dist/index.js --help
node dist/index.js help
node dist/index.js run hello.prose.md --harness mock
PROSE_HARNESS=mock node dist/index.js status
npm-package-smoke:
name: npm package smoke
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: tools/cli
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: tools/cli/package-lock.json
- name: Install dependencies
run: npm ci
- name: Pack npm artifact
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/npm-pack"
npm pack --pack-destination "$RUNNER_TEMP/npm-pack" --json | tee npm-pack.json
- name: Validate npm package manifest
run: |
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
const [pack] = JSON.parse(readFileSync("npm-pack.json", "utf8"));
const paths = new Set(pack.files.map((file) => file.path));
const required = ["package.json", "README.md", "LICENSE", "dist/index.js", "dist/index.d.ts"];
const forbiddenPrefixes = ["src/", "tests/", "node_modules/"];
for (const path of required) {
if (!paths.has(path)) {
throw new Error(`npm package is missing ${path}`);
}
}
for (const path of paths) {
if (forbiddenPrefixes.some((prefix) => path.startsWith(prefix))) {
throw new Error(`npm package includes ${path}`);
}
if (path.endsWith(".map")) {
throw new Error(`npm package includes sourcemap ${path}`);
}
}
EOF
- name: Smoke npm package artifact
run: |
set -euo pipefail
pack_file="$(node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
const [pack] = JSON.parse(readFileSync("npm-pack.json", "utf8"));
console.log(`${process.env.RUNNER_TEMP}/npm-pack/${pack.filename}`);
EOF
)"
prefix="$RUNNER_TEMP/npm-prefix"
npm install --global --prefix "$prefix" "$pack_file"
"$prefix/bin/prose" --version
"$prefix/bin/prose" run hello.prose.md --harness mock
import_dir="$RUNNER_TEMP/npm-import-smoke"
mkdir -p "$import_dir"
cd "$import_dir"
npm init -y
npm install "$pack_file"
node --input-type=module -e "await import('@openprose/prose-cli')"
- name: Dry-run npm publish
run: npm publish --dry-run
- name: Upload npm package metadata
uses: actions/upload-artifact@v4
with:
name: cli-npm-pack
path: tools/cli/npm-pack.json
if-no-files-found: error
release-tarball-smoke:
name: Release tarball smoke on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
defaults:
run:
working-directory: tools/cli
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: tools/cli/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Build and smoke release tarball
run: |
set -euo pipefail
release_dir="$RUNNER_TEMP/prose-release"
install_dir="$RUNNER_TEMP/prose-install"
bin_dir="$RUNNER_TEMP/prose-bin"
npm run release:tarball -- --out-dir "$release_dir" --skip-build
env \
PROSE_VERSION="$(node -p 'require("./package.json").version')" \
PROSE_BASE_URL="file://$release_dir" \
PROSE_INSTALL_DIR="$install_dir" \
PROSE_BIN_DIR="$bin_dir" \
sh install.sh
"$bin_dir/prose" --version
"$bin_dir/prose" run hello.prose.md --harness mock
- name: Upload release tarball
uses: actions/upload-artifact@v4
with:
name: cli-release-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/prose-release/*
if-no-files-found: error
production-audit:
name: Production dependency audit
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: tools/cli
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: tools/cli/package-lock.json
- name: Install dependencies
run: npm ci
- name: Audit production dependencies
run: npm run audit:policy