From af34d3a15e8ce152c59a0e87d4d2253fa2baeaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 16:10:26 +0200 Subject: [PATCH 01/10] chore: finalize pharn rename and disable provenance for local publish Rename remaining `pharn-cli` -> `pharn` references across docs, config, dev artifacts, and source comments: repo/bugs/homepage URLs in package.json, plus README, THREAT-MODEL, LIMITS, SECURITY, CHANGELOG, CONSTITUTION, CONTRIBUTING, docs/, and .dev/ plan artifacts. Set publishConfig.provenance to false so `npm publish` runs from a local machine. npm's automatic provenance requires a supported CI provider (GitHub Actions / GitLab OIDC) and aborts with "provider: null" when run locally. A follow-up CI workflow re-enables provenance via an explicit --provenance flag that overrides this default. Co-Authored-By: Claude Opus 4.8 --- .claude/commands/pharn-dev-build.md | 2 +- .claude/hooks/enforce-writes-scope.cjs | 2 +- .claude/hooks/enforce-writes-scope.test.cjs | 4 +- .dev/features/archetype-enum-align/PLAN.md | 4 +- .../features/archetype-file-tree-scan/PLAN.md | 2 +- .dev/features/archetype-io-boundary/PLAN.md | 2 +- .../features/archetype-missing-signal/PLAN.md | 2 +- .dev/features/archetype-path-context/PLAN.md | 2 +- .dev/features/capability-resolver/PLAN.md | 8 +-- .dev/features/commands-off-manifest/PLAN.md | 4 +- .dev/features/commands-off-manifest/REVIEW.md | 6 +- .dev/features/commands-off-manifest/SHIP.md | 2 +- .dev/features/commands-off-manifest/VERIFY.md | 6 +- .../commands-off-manifest/verify-report.json | 2 +- .../config-loader-honest-errors/GRILL.md | 2 +- .../config-loader-honest-errors/PLAN.md | 2 +- .dev/features/fresh-check-git-rce/PLAN.md | 10 ++-- .dev/features/harden-install-path/GRILL.md | 6 +- .dev/features/harden-install-path/PLAN.md | 4 +- .dev/features/harden-install-path/REVIEW.md | 4 +- .dev/features/harden-install-path/VERIFY.md | 2 +- .dev/features/init-archetype-default/PLAN.md | 2 +- .../features/init-archetype-default/REVIEW.md | 2 +- .../init-install-capabilities/GRILL.md | 2 +- .../init-install-capabilities/PLAN.md | 2 +- .dev/features/installer-layout-mirror/PLAN.md | 2 +- .dev/features/installer-layout-mirror/SHIP.md | 2 +- .dev/features/list-archetype-aware/PLAN.md | 2 +- .dev/features/model-routing-config/PLAN.md | 6 +- .dev/features/npm-publish-metadata/GRILL.md | 4 +- .dev/features/npm-publish-metadata/PLAN.md | 18 +++--- .../npm-publish-metadata/REGRESSION.md | 2 +- .dev/features/npm-publish-metadata/SHIP.md | 2 +- .dev/features/npm-publish-metadata/VERIFY.md | 2 +- .dev/features/regress/PLAN.md | 2 +- .dev/features/remove-dead-docs-url/GRILL.md | 2 +- .dev/features/remove-dead-docs-url/PLAN.md | 2 +- .../remove-dead-docs-url/REGRESSION.md | 4 +- .dev/features/remove-vendor-skill/PLAN.md | 2 +- .../remove-vendor-skill/REGRESSION.md | 8 +-- .dev/floor/lens-scanner-map.json | 2 +- .dev/floor/lens-scanner-map.test.mjs | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/config.yml | 4 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .pharn/writes-scope.json | 16 ++---- CHANGELOG.md | 16 +++--- CLAUDE.md | 2 +- CODE_OF_CONDUCT.md | 2 +- CONSTITUTION.md | 6 +- CONTRIBUTING.md | 6 +- LIMITS.md | 16 +++--- README.md | 56 ++++++++++--------- SECURITY.md | 18 +++--- THREAT-MODEL.md | 24 ++++---- docs/README.md | 2 +- docs/commands/init.md | 2 +- docs/contributing.md | 10 ++-- docs/getting-started.md | 4 +- docs/troubleshooting.md | 2 +- package.json | 8 +-- src/types.ts | 4 +- 63 files changed, 176 insertions(+), 178 deletions(-) diff --git a/.claude/commands/pharn-dev-build.md b/.claude/commands/pharn-dev-build.md index 2e117f6..90015f2 100644 --- a/.claude/commands/pharn-dev-build.md +++ b/.claude/commands/pharn-dev-build.md @@ -57,7 +57,7 @@ For each file the plan names in `## Files`: - Determinism (P5): branches are membership tests; the terminal fallback is "ask", never a guess. > Building a PHARN **markdown capability** (frontmatter contract, `evals/cases` + `evals/expected`, -> `seal:`) applies only when the increment adds one. pharn-cli is TypeScript — its "capabilities" +> `seal:`) applies only when the increment adds one. pharn is TypeScript — its "capabilities" > are modules and its "evals" are vitest tests; the structural `validate.mjs` floor below stays > vacuously-green until a markdown capability is actually added. diff --git a/.claude/hooks/enforce-writes-scope.cjs b/.claude/hooks/enforce-writes-scope.cjs index 6ee4408..b886e0b 100644 --- a/.claude/hooks/enforce-writes-scope.cjs +++ b/.claude/hooks/enforce-writes-scope.cjs @@ -60,7 +60,7 @@ function resolveWriteTarget(p) { // the Write tool cannot self-escalate by editing the gate's input. const ALWAYS = [".pharn/**"]; -// Fail-closed allow-list used when no scope file is set. pharn-cli's editable code (src/, tests/) + +// Fail-closed allow-list used when no scope file is set. pharn's editable code (src/, tests/) + // the dev-loop's own build artifacts (.dev/features/) + the legacy root artifact zone (features/, // retained, harmless) + process scratch (.pharn/, via ALWAYS). The sensitive zones (.dev/floor/, // .dev/memory-bank/, .claude/, root spec docs, docs/, config) are intentionally absent — reaching diff --git a/.claude/hooks/enforce-writes-scope.test.cjs b/.claude/hooks/enforce-writes-scope.test.cjs index 4ea9155..bdf006d 100644 --- a/.claude/hooks/enforce-writes-scope.test.cjs +++ b/.claude/hooks/enforce-writes-scope.test.cjs @@ -41,7 +41,7 @@ function setter(cwd, ...args) { // --- Hook, no scope file: fail-closed default-safe-set --- -test("no scope: src/ (pharn-cli source) is ALLOWED", () => { +test("no scope: src/ (pharn source) is ALLOWED", () => { assert.equal(hook(tmp(), "src/lib/installer.ts").status, 0); }); @@ -49,7 +49,7 @@ test("no scope: tests/ is ALLOWED", () => { assert.equal(hook(tmp(), "tests/installer.test.ts").status, 0); }); -test("no scope: a pharn-oss module path (pharn-review/) is DENIED (pharn-cli has no module dirs — not in the safe-set)", () => { +test("no scope: a pharn-oss module path (pharn-review/) is DENIED (pharn has no module dirs — not in the safe-set)", () => { assert.equal(hook(tmp(), "pharn-review/foo.md").status, 2); }); diff --git a/.dev/features/archetype-enum-align/PLAN.md b/.dev/features/archetype-enum-align/PLAN.md index 3737ebb..5b56107 100644 --- a/.dev/features/archetype-enum-align/PLAN.md +++ b/.dev/features/archetype-enum-align/PLAN.md @@ -2,12 +2,12 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 (ARCHITECTURE.md) - increment: Map DB signals (`.sql` files, `migrations/` dirs, and `prisma` / `@prisma/client` / `drizzle-orm` deps) to the existing `backend` archetype — the one residual after discovery found the enum alignment already complete. **Approved by the human at the halt (this run); knowingly reverses the tested decision #2 of increment #21.** (The enum↔pharn-oss alignment itself was already implemented across #17/#20/#21 — confirmed, no change needed.) -- layer(s): pharn-cli `src/lib` capability-resolver (ARCHITECTURE.md §5, "Archetype + map-consistency") — no `pharn-contracts` / `pharn-core` module files touched +- layer(s): pharn `src/lib` capability-resolver (ARCHITECTURE.md §5, "Archetype + map-consistency") — no `pharn-contracts` / `pharn-core` module files touched - constitution_refs: [P6, P7, P5, P2, P3] ## Discovery finding — READ FIRST (the increment's premise is stale) -Everything the request describes as a *change* is **already in the tree** (verified by reads this run, not memory — P6). The task's premise — _"pharn-cli's archetype.ts currently uses {frontend, db, …} — mismatched"_ — is **false against live state**: +Everything the request describes as a *change* is **already in the tree** (verified by reads this run, not memory — P6). The task's premise — _"pharn's archetype.ts currently uses {frontend, db, …} — mismatched"_ — is **false against live state**: | Requested change | Live state | Evidence | | --- | --- | --- | diff --git a/.dev/features/archetype-file-tree-scan/PLAN.md b/.dev/features/archetype-file-tree-scan/PLAN.md index e8ba278..f0d6542 100644 --- a/.dev/features/archetype-file-tree-scan/PLAN.md +++ b/.dev/features/archetype-file-tree-scan/PLAN.md @@ -86,7 +86,7 @@ Signal → archetype mapping (the enum has no `db`/`frontend`, so names map onto ## Contracts satisfied - **N/A — no `pharn-contracts` reference.** `pharn-contracts` is a PHARN-*product* layer (ARCHITECTURE.md - §4); this is a pharn-cli-internal `src/lib/` boundary and satisfies no product contract (as with #20). + §4); this is a pharn-internal `src/lib/` boundary and satisfies no product contract (as with #20). - **Spec cited, not restated (P4):** ARCHITECTURE.md §5 (`archetype ∈ {ssr, backend, spa, lib}`, deterministic detection) and §4 (the frameworkless `lib` base). This increment **extends the detection INPUT surface** (adds file-tree signals) while preserving the enum and determinism; it does not restate the rule. diff --git a/.dev/features/archetype-io-boundary/PLAN.md b/.dev/features/archetype-io-boundary/PLAN.md index bd1e414..cd6e36c 100644 --- a/.dev/features/archetype-io-boundary/PLAN.md +++ b/.dev/features/archetype-io-boundary/PLAN.md @@ -50,7 +50,7 @@ deliberately **out of scope** here (P7 — smallest coherent increment). ## Contracts satisfied - **N/A — no `pharn-contracts` reference.** `pharn-contracts` is a PHARN-*product* layer (ARCHITECTURE.md - §4); this increment is a pharn-cli-internal `src/lib/` boundary and satisfies no product contract. + §4); this increment is a pharn-internal `src/lib/` boundary and satisfies no product contract. - **Spec cited, not restated (P4):** ARCHITECTURE.md §5 (`archetype ∈ {ssr, backend, spa, lib}`, "detected deterministically (membership over `package.json`)") and §4 (the frameworkless `lib` base). The new boundary adds the I/O around that rule; it does not re-implement or restate the rule. diff --git a/.dev/features/archetype-missing-signal/PLAN.md b/.dev/features/archetype-missing-signal/PLAN.md index ff96010..1e5959b 100644 --- a/.dev/features/archetype-missing-signal/PLAN.md +++ b/.dev/features/archetype-missing-signal/PLAN.md @@ -33,7 +33,7 @@ refactor, out of scope now (P7). ## Contracts satisfied -- **N/A — no `pharn-contracts` reference** (a pharn-cli-internal `src/lib/` boundary, as in the prior +- **N/A — no `pharn-contracts` reference** (a pharn-internal `src/lib/` boundary, as in the prior increment). - **Spec cited, not restated (P4):** ARCHITECTURE.md §5 (`archetype ∈ {ssr, backend, spa, lib}`, "membership over `package.json`") and §4 (frameworkless `lib` base). The change only enriches the diff --git a/.dev/features/archetype-path-context/PLAN.md b/.dev/features/archetype-path-context/PLAN.md index 9ddad68..bb6415b 100644 --- a/.dev/features/archetype-path-context/PLAN.md +++ b/.dev/features/archetype-path-context/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 (ARCHITECTURE.md) - increment: Give `classifyEntry` (detect-archetype.ts) the **path context** its four file-tree rules already assume in their comments — scope `api/`, `route.*`, `.sql`, and `.tsx/.jsx` matches to their documented locations so a file merely _named_ x no longer implies the project _has_ surface x; and (secondary) loosen `parseApplies` (capability-index.ts) to accept unquoted YAML `applies` tokens. No new archetype/enum member; determinism preserved. -- layer(s): pharn-cli `src/lib` — the archetype-detection **I/O boundary** (`detect-archetype.ts`) and the capability **fetch boundary** (`capability-index.ts`), both under ARCHITECTURE.md §5 "Archetype + map-consistency" / "Trust-fence". No `pharn-contracts` / `pharn-core` / `ARCHITECTURE.md` files touched (§5 is hook-protected + human-only). +- layer(s): pharn `src/lib` — the archetype-detection **I/O boundary** (`detect-archetype.ts`) and the capability **fetch boundary** (`capability-index.ts`), both under ARCHITECTURE.md §5 "Archetype + map-consistency" / "Trust-fence". No `pharn-contracts` / `pharn-core` / `ARCHITECTURE.md` files touched (§5 is hook-protected + human-only). - constitution_refs: [P5, P2, P6, P7, P1, P3] ## Discovery findings — READ FIRST (grounded in reads this run, not memory — P6) diff --git a/.dev/features/capability-resolver/PLAN.md b/.dev/features/capability-resolver/PLAN.md index 1c85c20..ccb842c 100644 --- a/.dev/features/capability-resolver/PLAN.md +++ b/.dev/features/capability-resolver/PLAN.md @@ -1,8 +1,8 @@ # PLAN — capability-resolver (archetype detection → capability selection, pure core) - spec_content_hash: 11cd9ad5983188623fe0931d13588c16435a5565888344e20669748947d1d969 # fix #4 — sha256 of ARCHITECTURE.md, read this run -- increment: Add pharn-cli's **pure, deterministic** archetype-detection + capability-selection core — `(package.json, capability-index) → { selected, skipped }` — with its test suite. **No fetch, no install, no command/hook/doc copying** (all deferred). -- layer(s): **product** (`pharn-cli` CLI, `src/`) — NOT a `pharn-*` methodology Capability. This is the installer's selection logic, consumer of the pharn-oss capability catalog. +- increment: Add pharn's **pure, deterministic** archetype-detection + capability-selection core — `(package.json, capability-index) → { selected, skipped }` — with its test suite. **No fetch, no install, no command/hook/doc copying** (all deferred). +- layer(s): **product** (`pharn` CLI, `src/`) — NOT a `pharn-*` methodology Capability. This is the installer's selection logic, consumer of the pharn-oss capability catalog. - constitution_refs: [P3, P5, P6, P7, P0] ## Decisions carried in (human-selected at the plan HALT, this run) @@ -15,7 +15,7 @@ - pharn-oss has **no `manifest.json`/`module.json`** anymore (only `SKILLS_VERSION`); it ships capabilities: `pharn-pipeline/grillers//.md` (11) + `pharn-review//.md` (22), each with `evals/`. - **Every** capability frontmatter is `coupling: agnostic` with **no** archetype/stack/`applies_when` field — applicability is **not** declared anywhere machine-readable today. Hence decision #1 (a new index). -- `pharn-contracts/` (both repos) has only `eval-format.md`, `finding-shape.md`, `seam-config.md` — **no archetype enum, no index contract** exists yet, though `ARCHITECTURE.md §4` promises them. So the `Archetype` enum + index shape are defined **consumer-side in pharn-cli** for now (mirrors how `src/lib/manifest.ts` already owns pharn-cli's parse of a pharn-oss-owned schema). Coordination note, not a blocker. +- `pharn-contracts/` (both repos) has only `eval-format.md`, `finding-shape.md`, `seam-config.md` — **no archetype enum, no index contract** exists yet, though `ARCHITECTURE.md §4` promises them. So the `Archetype` enum + index shape are defined **consumer-side in pharn** for now (mirrors how `src/lib/manifest.ts` already owns pharn's parse of a pharn-oss-owned schema). Coordination note, not a blocker. - No existing `archetype` code in `src/` (greenfield). Product floor = **`npm run check`** (`format:check && lint && typecheck && test`); `.dev/floor/validate.mjs` walks only `.md` capabilities and **excludes `src/`** — so this increment's deterministic floor is the **vitest suite + tsc + eslint + prettier**, not `validate.mjs`. ## Files @@ -43,7 +43,7 @@ Multi-archetype is intentional and deterministic (e.g. Next + Express ⇒ `[ssr, - **`ARCHITECTURE.md §5` (archetype + map-consistency)** — implements "archetype ∈ {ssr,backend,spa,lib}, detected deterministically (membership over `package.json`)" as a pure function. Cited, not restated (P4). - **`ARCHITECTURE.md §4`** — "A frameworkless lib runs on core alone" ⇒ the `lib` empty-set fallback. Cited (P4). -- **(future) capability-index schema** — this increment defines the **consumer-side** shape pharn-cli will parse; the **authoritative** schema stays pharn-oss's to own (P4). Named as a coordination point, not claimed as satisfied here. +- **(future) capability-index schema** — this increment defines the **consumer-side** shape pharn will parse; the **authoritative** schema stays pharn-oss's to own (P4). Named as a coordination point, not claimed as satisfied here. ## Evals to write (P1 — for product TS, the vitest suite IS the eval/spec) diff --git a/.dev/features/commands-off-manifest/PLAN.md b/.dev/features/commands-off-manifest/PLAN.md index 8b36843..f729d7d 100644 --- a/.dev/features/commands-off-manifest/PLAN.md +++ b/.dev/features/commands-off-manifest/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 (ARCHITECTURE.md) - increment: Migrate `add`/`list`/`remove`/`status`/`update` fully onto the archetype/capability model + `pharn/` layout, then delete the now-orphaned manifest/module/wizard install path — closing Fable's symlink finding (`install-modules.ts`). -- layer(s): pharn-cli (the installer) — `src/commands/*`, `src/lib/*`, `src/steps/*`, `tests/*`, docs +- layer(s): pharn (the installer) — `src/commands/*`, `src/lib/*`, `src/steps/*`, `tests/*`, docs - constitution_refs: [P0, P1, P2, P3, P4, P5, P6, P7] ## Context (live state, verified this run) @@ -119,7 +119,7 @@ message, decision Q1). Single-source the reject via a shared helper (grill F4/P3 archetype paths resolve live now. - `CONSTITUTION.md` P2/P3/P5/P6 — preserved by construction (see audits below). -## Evals to write (P1) # pharn-cli's evals ARE its vitest tests (P1: "tests are the spec") +## Evals to write (P1) # pharn's evals ARE its vitest tests (P1: "tests are the spec") - `safeJoin` (relocated) → path escaping `..`/absolute/outside-base → throws; in-base → returns joined path. - `add` (archetype) → `add lens:n-plus-one` copies one capability + appends to config; already-installed → noop. diff --git a/.dev/features/commands-off-manifest/REVIEW.md b/.dev/features/commands-off-manifest/REVIEW.md index 0375a7a..221632b 100644 --- a/.dev/features/commands-off-manifest/REVIEW.md +++ b/.dev/features/commands-off-manifest/REVIEW.md @@ -63,11 +63,11 @@ Advisory: a one-line comment in an out-of-scope file; fold into the same follow- rule_id: P0 severity: minor file: ".dev/features/commands-off-manifest/VERIFY.md:1" - problem: "pharn-cli's deterministic floor is `npm run check`, NOT `.dev/floor/validate.mjs .` — the latter always RED at the repo root because it scans untracked test-app red fixtures. The dev-loop stage commands assume validate.mjs is the build/verify floor." - evidence: "validate.mjs . → RED from test-app/test-fixtures/red/skill.md, unrelated to any pharn-cli source change." + problem: "pharn's deterministic floor is `npm run check`, NOT `.dev/floor/validate.mjs .` — the latter always RED at the repo root because it scans untracked test-app red fixtures. The dev-loop stage commands assume validate.mjs is the build/verify floor." + evidence: "validate.mjs . → RED from test-app/test-fixtures/red/skill.md, unrelated to any pharn source change." ``` -Advisory + a doc-reconciliation for the human: for a TS installer repo like pharn-cli, the ship/build/ +Advisory + a doc-reconciliation for the human: for a TS installer repo like pharn, the ship/build/ verify stages' "read validate.mjs" instruction maps to `npm run check`. Surfaced consistently across GRILL/REGRESSION/VERIFY; not a defect in the increment. diff --git a/.dev/features/commands-off-manifest/SHIP.md b/.dev/features/commands-off-manifest/SHIP.md index a7278b2..2b02b38 100644 --- a/.dev/features/commands-off-manifest/SHIP.md +++ b/.dev/features/commands-off-manifest/SHIP.md @@ -29,7 +29,7 @@ symlink-write finding. **exit 1**, but exclusively from untracked `test-app/test-fixtures/red/skill.md` (a deliberately-red scratch fixture, pre-existing, unrelated to this increment); the `/pharn-dev-build` command states validate.mjs "gates nothing" for an increment adding no PHARN markdown capability (this one adds none). - For pharn-cli the deterministic build floor is `npm run check`, and it is GREEN. + For pharn the deterministic build floor is `npm run check`, and it is GREEN. - **`/pharn-dev-regress` → `regression-report.json` `.verdict`:** `"no-regressions"` (the outside `.mjs/.cjs` suite is RED at both base and HEAD — pre-existing, identical, no pass→fail flip; `validate` excluded as confounded by untracked `test-app/`; style gates skipped — config untouched). diff --git a/.dev/features/commands-off-manifest/VERIFY.md b/.dev/features/commands-off-manifest/VERIFY.md index 8ae9739..a15e4d5 100644 --- a/.dev/features/commands-off-manifest/VERIFY.md +++ b/.dev/features/commands-off-manifest/VERIFY.md @@ -13,7 +13,7 @@ | `lint:md` | 0 | markdownlint clean (docs + root markdown) | `check-verify.mjs` verdict: **PASS** (every gate exit 0), `failing_gates: []`. This gate set is exactly -the repo's `npm run check` aggregate (+ `lint:md`) — the floor CLAUDE.md/CI define for pharn-cli. +the repo's `npm run check` aggregate (+ `lint:md`) — the floor CLAUDE.md/CI define for pharn. ## Gate deliberately excluded (documented, not silent) @@ -21,10 +21,10 @@ the repo's `npm run check` aggregate (+ `lint:md`) — the floor CLAUDE.md/CI de findings are in **untracked `test-app/`** (`test-fixtures/red/skill.md`, a *deliberately-red* fixture that `validate.test.mjs` uses to prove the checker catches red capabilities). That RED is unrelated to this increment — which changed **zero** capabilities, **zero** `.dev/floor/`, and **zero** `test-app/` - files — and to pharn-cli's own source. pharn-cli's CI floor is `npm run check` (CLAUDE.md), not + files — and to pharn's own source. pharn's CI floor is `npm run check` (CLAUDE.md), not `validate.mjs`; the `/pharn-dev-build` command itself states `validate.mjs` "gates nothing" for an increment that adds no PHARN markdown capability (this one adds none). Including it would make *every* - pharn-cli increment fail verify on unrelated scratch — a false RED. The exclusion is the verify + pharn increment fail verify on unrelated scratch — a false RED. The exclusion is the verify command's sanctioned "gate SET is advisory orchestration" latitude, applied to a confounded gate, and surfaced here rather than hidden. diff --git a/.dev/features/commands-off-manifest/verify-report.json b/.dev/features/commands-off-manifest/verify-report.json index 606c870..7b53370 100644 --- a/.dev/features/commands-off-manifest/verify-report.json +++ b/.dev/features/commands-off-manifest/verify-report.json @@ -13,7 +13,7 @@ "excluded_gates": { "validate": { "measured_exit": 1, - "reason": "validate.mjs . scans untracked test-app/ scratch (test-fixtures/red/skill.md — a deliberately-red fixture); its RED is unrelated to this increment (which changed zero capabilities, zero .dev/floor/, zero test-app/) and to pharn-cli source. pharn-cli's CI floor is `npm run check` (CLAUDE.md), which is GREEN and fully represented above. The build command states validate.mjs 'gates nothing' for an increment that adds no PHARN markdown capability." + "reason": "validate.mjs . scans untracked test-app/ scratch (test-fixtures/red/skill.md — a deliberately-red fixture); its RED is unrelated to this increment (which changed zero capabilities, zero .dev/floor/, zero test-app/) and to pharn source. pharn's CI floor is `npm run check` (CLAUDE.md), which is GREEN and fully represented above. The build command states validate.mjs 'gates nothing' for an increment that adds no PHARN markdown capability." } } } diff --git a/.dev/features/config-loader-honest-errors/GRILL.md b/.dev/features/config-loader-honest-errors/GRILL.md index 52bbac7..492136a 100644 --- a/.dev/features/config-loader-honest-errors/GRILL.md +++ b/.dev/features/config-loader-honest-errors/GRILL.md @@ -55,7 +55,7 @@ grillers: `count-grillers.mjs` → `0` (no external griller axes to run; inline rule_id: "P4" severity: important file: ".dev/features/config-loader-honest-errors/PLAN.md:83" - problem: "pharn-cli is a PUBLISHED CLI and this stricter validation now REJECTS configs that previously loaded (extra/typo'd keys in models/seam) and hard-errors instead of 'run init' — a user-visible, potentially surprising change — yet CHANGELOG.md is absent from the Files list." + problem: "pharn is a PUBLISHED CLI and this stricter validation now REJECTS configs that previously loaded (extra/typo'd keys in models/seam) and hard-errors instead of 'run init' — a user-visible, potentially surprising change — yet CHANGELOG.md is absent from the Files list." evidence: "'docs/troubleshooting.md — distinguish absent … from present-but-invalid' — the only doc touched; no CHANGELOG entry." ``` diff --git a/.dev/features/config-loader-honest-errors/PLAN.md b/.dev/features/config-loader-honest-errors/PLAN.md index 5c4b938..0d1deaa 100644 --- a/.dev/features/config-loader-honest-errors/PLAN.md +++ b/.dev/features/config-loader-honest-errors/PLAN.md @@ -6,7 +6,7 @@ keys, duplicate steps, and a dead threshold, naming the offender; (3) the loader returns the validators' typed result, not `raw`; keeping the CLI validators, the parallel floor validator, and the seam contract in lockstep. -- layer(s): pharn-cli — lib/ (loader + both validators) · commands/ + steps/ (callers) · pharn-contracts +- layer(s): pharn — lib/ (loader + both validators) · commands/ + steps/ (callers) · pharn-contracts (seam SoT) · .dev/floor (parallel validator) · docs/ (P4). - constitution_refs: [P0, P1, P2, P3, P4, P5, P6, P7] - scope note: LARGE but coherent — one theme (honest+strict config load/validate). Human directed "fix diff --git a/.dev/features/fresh-check-git-rce/PLAN.md b/.dev/features/fresh-check-git-rce/PLAN.md index 1d22e6d..c15d7a5 100644 --- a/.dev/features/fresh-check-git-rce/PLAN.md +++ b/.dev/features/fresh-check-git-rce/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 (ARCHITECTURE.md) - increment: Neutralize attacker-controlled git-config-driven code execution in `fresh-check.ts` by routing every git invocation through one hardened, shell-free helper. -- layer(s): pharn-cli `src/steps/` (one init stage) + `tests/` — this is a CLI-source security fix, not a new PHARN Capability. +- layer(s): pharn `src/steps/` (one init stage) + `tests/` — this is a CLI-source security fix, not a new PHARN Capability. - constitution_refs: [P2, P1, P5, P6, P7] ## The bug (grounded this run, not from memory — P6) @@ -48,7 +48,7 @@ functions still return the same integers (verified). `execFileSync`-based `runGit` helper that always injects the `-c core.fsmonitor=`/`core.hooksPath` hardening; `gitCommitCount` → `runGit(['rev-list','--count','HEAD'], cwd)`, `gitTrackedFileCount` → `runGit(['ls-files'], cwd)`. Behavior (returns, thresholds, `catch → 0` fallback) unchanged. — - layer: pharn-cli `steps/` (init stage). + layer: pharn `steps/` (init stage). - `tests/fresh-check.test.ts` — add a regression test: inject a malicious `core.fsmonitor` (touch a sentinel; `false`) into a fixture repo's `.git/config`, then assert the production functions (`gitTrackedFileCount`, `gitCommitCount`, `runFreshCheck`) do **not** create the sentinel (no @@ -60,7 +60,7 @@ functions still return the same integers (verified). - CONSTITUTION.md **P2** (untrusted input is data, never trusted input) — the target repo's `.git/config` is untrusted; after the fix it can influence git *output* but never *execution* - (`THREAT-MODEL.md §1` Surface B / B′: pharn-cli itself consuming hostile local input). Cite, not + (`THREAT-MODEL.md §1` Surface B / B′: pharn itself consuming hostile local input). Cite, not restate (P4). - CONSTITUTION.md **P1** (tests are the spec) — the security invariant ships with a test that exercises it. @@ -68,7 +68,7 @@ functions still return the same integers (verified). ## Evals to write (P1) This increment adds no PHARN Capability/rule, so `floor/validate.mjs`'s eval-binding does not apply. -The P1 obligation for a pharn-cli **security invariant** is a `vitest` test that *demonstrates* it +The P1 obligation for a pharn **security invariant** is a `vitest` test that *demonstrates* it (CONSTITUTION P1). The regression test above is that demonstration: - malicious `core.fsmonitor` present → `gitTrackedFileCount` runs → sentinel file is **NOT** created @@ -82,7 +82,7 @@ The P1 obligation for a pharn-cli **security invariant** is a `vitest` test that - **Claim:** "fresh-check's git calls no longer execute attacker-controlled `.git/config` (`core.fsmonitor` / hooks)." → **floor: test (deterministic `existsSync(sentinel) === false`) + single-call-site code structure** (grep-verified: `fresh-check.ts` is the only shell-out in - `src/`; both calls route through the one hardening helper). This is exactly how pharn-cli floors + `src/`; both calls route through the one hardening helper). This is exactly how pharn floors its own security invariants (P1: "every security invariant has a test that demonstrates the behavior"). Not one of the three PHARN methodology floor primitives (§2) — those govern the installed methodology, not the CLI's own source. diff --git a/.dev/features/harden-install-path/GRILL.md b/.dev/features/harden-install-path/GRILL.md index dc3ebb7..2ae57af 100644 --- a/.dev/features/harden-install-path/GRILL.md +++ b/.dev/features/harden-install-path/GRILL.md @@ -22,9 +22,9 @@ rule_id: P1 severity: important file: ".dev/features/harden-install-path/PLAN.md:70" - problem: "The 'degit fails closed if it cannot fetch that sha' half of the FIX 1 floor claim has no planned eval — the repo.test.ts cases mock degit to capture the src argument, so they cannot demonstrate fail-closed; the property rests on a source-read of degit this run, not on a pharn-cli test." + problem: "The 'degit fails closed if it cannot fetch that sha' half of the FIX 1 floor claim has no planned eval — the repo.test.ts cases mock degit to capture the src argument, so they cannot demonstrate fail-closed; the property rests on a source-read of degit this run, not on a pharn test." evidence: "\"degit fails closed if it cannot fetch that sha. Testable (P1).\" — but the two repo.test.ts cases (:60-61) stub degit, exercising only the ref STRING passed, not degit's real resolution." - # resolution (build): either scope 'fail-closed' as DEGIT behavior (source-grounded, advisory — not a pharn-cli-tested guarantee), or drop it from the floor claim and rest the floor on the tested 'recorded == resolved-sha XOR null' property only. + # resolution (build): either scope 'fail-closed' as DEGIT behavior (source-grounded, advisory — not a pharn-tested guarantee), or drop it from the floor claim and rest the floor on the tested 'recorded == resolved-sha XOR null' property only. - type: FINDING rule_id: P2 @@ -56,7 +56,7 @@ The concerns cluster on **FIX 1**, and both important findings are *precision*, Two minor suggestions (a check-then-write honesty note; a shared-helper refactor to pre-empt copy-paste drift) are for build/human to weigh. -**On eval shape:** the plan's "evals" are `vitest` tests — correct for pharn-cli code (CONSTITUTION P1). `eval-format.md`'s `structural[]`/`semantic[]` split governs **methodology-capability** evals (`{case, expected}` markdown skills), **not** CLI unit tests, so it does not strictly apply; every planned assertion is deterministic (`toThrow`, `existsSync`, captured `degit` arg) with **zero** LLM judge — the ideal the split exists to protect. Not a finding. +**On eval shape:** the plan's "evals" are `vitest` tests — correct for pharn code (CONSTITUTION P1). `eval-format.md`'s `structural[]`/`semantic[]` split governs **methodology-capability** evals (`{case, expected}` markdown skills), **not** CLI unit tests, so it does not strictly apply; every planned assertion is deterministic (`toThrow`, `existsSync`, captured `degit` arg) with **zero** LLM judge — the ideal the split exists to protect. Not a finding. **On scope (P7):** the plan raised the bundle-vs-split question itself (OQ1) and the human resolved it at GATE 1 (bundle all three). Not re-litigated here. diff --git a/.dev/features/harden-install-path/PLAN.md b/.dev/features/harden-install-path/PLAN.md index 053d694..1807d40 100644 --- a/.dev/features/harden-install-path/PLAN.md +++ b/.dev/features/harden-install-path/PLAN.md @@ -1,8 +1,8 @@ # PLAN — harden-install-path - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # ARCHITECTURE.md (fix #4) -- increment: Close the three Fable install-path findings on pharn-cli — a symlink-escape arbitrary-write hole and a missing dev/product allowlist on the LEGACY copy path (`install-modules.ts`), plus a floating-`main` degit fetch (`repo.ts`) whose recorded `commit` can diverge from the fetched bytes. -- layer(s): pharn-cli `lib/` (`repo.ts`, `install-modules.ts`, `installer.ts`) + `commands/` (`init.ts`, `add.ts`, `update.ts`). NB — ARCHITECTURE.md §4's layer tree describes the PHARN **methodology** modules, not the installer's TypeScript source; installer-source layering is **CONSTITUTION P3** (`index → commands → steps → lib`, no sibling imports). All shared logic here stays in `lib/`. +- increment: Close the three Fable install-path findings on pharn — a symlink-escape arbitrary-write hole and a missing dev/product allowlist on the LEGACY copy path (`install-modules.ts`), plus a floating-`main` degit fetch (`repo.ts`) whose recorded `commit` can diverge from the fetched bytes. +- layer(s): pharn `lib/` (`repo.ts`, `install-modules.ts`, `installer.ts`) + `commands/` (`init.ts`, `add.ts`, `update.ts`). NB — ARCHITECTURE.md §4's layer tree describes the PHARN **methodology** modules, not the installer's TypeScript source; installer-source layering is **CONSTITUTION P3** (`index → commands → steps → lib`, no sibling imports). All shared logic here stays in `lib/`. - constitution_refs: [P0, P1, P2, P5, P6, P7] --- diff --git a/.dev/features/harden-install-path/REVIEW.md b/.dev/features/harden-install-path/REVIEW.md index 00a507b..0bdc700 100644 --- a/.dev/features/harden-install-path/REVIEW.md +++ b/.dev/features/harden-install-path/REVIEW.md @@ -48,9 +48,9 @@ rule_id: P1 severity: minor file: "src/lib/repo.ts:35" - problem: "The 'degit fails closed if REPO_BRANCH moves mid-fetch' property (part of FIX 1's honesty) is documented as DEGIT behavior from a source-read this run, and is not covered by a pharn-cli test (repo.test.ts mocks degit)." + problem: "The 'degit fails closed if REPO_BRANCH moves mid-fetch' property (part of FIX 1's honesty) is documented as DEGIT behavior from a source-read this run, and is not covered by a pharn test (repo.test.ts mocks degit)." evidence: "and if REPO_BRANCH moves mid-fetch degit's own resolution fails (the SHA is no longer a ref tip) rather than fetching drift" - # Correctly SCOPED as degit's behavior (not a pharn-cli guarantee) per the grill's P1 finding — + # Correctly SCOPED as degit's behavior (not a pharn guarantee) per the grill's P1 finding — # reflected honestly in the comment. Testing it would test degit (out of scope). Noted for the human. - type: FINDING diff --git a/.dev/features/harden-install-path/VERIFY.md b/.dev/features/harden-install-path/VERIFY.md index 70128be..aa0ed30 100644 --- a/.dev/features/harden-install-path/VERIFY.md +++ b/.dev/features/harden-install-path/VERIFY.md @@ -4,7 +4,7 @@ ## FLOOR layer — deterministic gates (owns the verdict) -The gate set mirrors this repo's enforced floor — `npm run check` (`format:check → lint → typecheck → test`) plus the structural `validate`. `lint:md` is **not** part of pharn-cli's `npm run check`/CI and its globs (`docs/**/*.md`, `*.md`) cover none of this increment's files, so it is not a verify gate here. +The gate set mirrors this repo's enforced floor — `npm run check` (`format:check → lint → typecheck → test`) plus the structural `validate`. `lint:md` is **not** part of pharn's `npm run check`/CI and its globs (`docs/**/*.md`, `*.md`) cover none of this increment's files, so it is not a verify gate here. | gate | exit | meaning | | -------------- | ---- | --------------------------------------------------- | diff --git a/.dev/features/init-archetype-default/PLAN.md b/.dev/features/init-archetype-default/PLAN.md index 4319304..44729bd 100644 --- a/.dev/features/init-archetype-default/PLAN.md +++ b/.dev/features/init-archetype-default/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 - increment: `npx pharn init` (no flag) runs the archetype/capability flow by default; the init-only legacy wizard flow (`runInitLegacy`/`runInitV2`/`loadManifest` and its 9 orphaned step files) is deleted. -- layer(s): pharn-cli product surface (`src/commands/init.ts`, `src/index.ts`, `src/steps/*`) — not a PHARN methodology layer. +- layer(s): pharn product surface (`src/commands/init.ts`, `src/index.ts`, `src/steps/*`) — not a PHARN methodology layer. - constitution_refs: [P0, P5, P6, P7] ## Discovery findings (live state, this run) diff --git a/.dev/features/init-archetype-default/REVIEW.md b/.dev/features/init-archetype-default/REVIEW.md index 6db72f6..ce1d72a 100644 --- a/.dev/features/init-archetype-default/REVIEW.md +++ b/.dev/features/init-archetype-default/REVIEW.md @@ -31,7 +31,7 @@ The increment emits no findings/free-text and ingests no new untrusted input — ### L-axis → P3 — GREEN -Each changed file carries one coherent axis: `init.ts` (make archetype the default + delete legacy), `index.ts` (flag no-op + honest help + call change). No cross-sibling-module reference (pharn-cli is a single package; no `pharn-contracts` routing applies). The A-clean bundle (behavior change + dead-code sweep) is a P7-scope choice, human-accepted at GATE 1 — not a P3 file-axis violation. +Each changed file carries one coherent axis: `init.ts` (make archetype the default + delete legacy), `index.ts` (flag no-op + honest help + call change). No cross-sibling-module reference (pharn is a single package; no `pharn-contracts` routing applies). The A-clean bundle (behavior change + dead-code sweep) is a P7-scope choice, human-accepted at GATE 1 — not a P3 file-axis violation. ## Advisory findings (inform the GATE-2 decision; block nothing) diff --git a/.dev/features/init-install-capabilities/GRILL.md b/.dev/features/init-install-capabilities/GRILL.md index 4d69652..ba73b78 100644 --- a/.dev/features/init-install-capabilities/GRILL.md +++ b/.dev/features/init-install-capabilities/GRILL.md @@ -2,7 +2,7 @@ Plan interrogated: `.dev/features/init-install-capabilities/PLAN.md`. Spec-hash check: **MATCH** (`sha256(ARCHITECTURE.md)` = `bca940a5…` == plan `spec_content_hash`; no drift). Registered grillers: -`count-grillers.mjs .` → **0** (pharn-cli is the CLI repo, not a methodology-capability repo) → inline +`count-grillers.mjs .` → **0** (pharn is the CLI repo, not a methodology-capability repo) → inline interrogation only. **This whole log is ADVISORY — it gates nothing** (`/pharn-dev-build` is not blocked by any finding here). Free-text `problem`/`evidence` quote the (untrusted) plan as DATA. diff --git a/.dev/features/init-install-capabilities/PLAN.md b/.dev/features/init-install-capabilities/PLAN.md index eb8f9d7..85c01d3 100644 --- a/.dev/features/init-install-capabilities/PLAN.md +++ b/.dev/features/init-install-capabilities/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 — sha256 of ARCHITECTURE.md, read this run - increment: Wire the existing pure **detect** (`detect-archetype.ts`) + **resolve** (`resolve-capabilities.ts`) into a **working, flag-gated** `pharn init --archetype`: fetch pharn-oss, derive a validated capability index from capability frontmatter, resolve against detected archetypes, show selected/skipped + confirm, copy the product surfaces (excluding dev-only), and write `pharn.config.json`. -- layer(s): **product** (`pharn-cli` CLI, `src/`) — the installer, not a `pharn-*` methodology Capability. +- layer(s): **product** (`pharn` CLI, `src/`) — the installer, not a `pharn-*` methodology Capability. - constitution_refs: [P0, P1, P2, P3, P5, P6, P7] ## Decisions carried in (human-selected at this plan's HALT, this run) diff --git a/.dev/features/installer-layout-mirror/PLAN.md b/.dev/features/installer-layout-mirror/PLAN.md index 8165174..6a9e43e 100644 --- a/.dev/features/installer-layout-mirror/PLAN.md +++ b/.dev/features/installer-layout-mirror/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 (ARCHITECTURE.md) - increment: Make the CLI **mirror whichever layout the fetched pharn-oss clone has** — the new `pharn/` single-install layout (pharn-oss PR #86 / `pharn-runtime-layout`) OR the current flat layout — across ALL archetype surfaces (install, status/drift, remove), via one deterministic layout resolver, recording the installed layout in `pharn.config.json`. Scope: all-at-once (write + read side) per GATE-1 decision; clone ref stays `main`. -- layer(s): pharn-cli product source — `src/lib/` (resolver, constants, types, capability-index, install-capabilities, diff, pharn-config) + `src/steps/` (install-archetype) + `src/commands/` (status, remove). ARCHITECTURE.md §4. +- layer(s): pharn product source — `src/lib/` (resolver, constants, types, capability-index, install-capabilities, diff, pharn-config) + `src/steps/` (install-archetype) + `src/commands/` (status, remove). ARCHITECTURE.md §4. - constitution_refs: [P7, P5, P2, P1, P3] ## Files diff --git a/.dev/features/installer-layout-mirror/SHIP.md b/.dev/features/installer-layout-mirror/SHIP.md index 3160c82..c76738b 100644 --- a/.dev/features/installer-layout-mirror/SHIP.md +++ b/.dev/features/installer-layout-mirror/SHIP.md @@ -4,7 +4,7 @@ Thin, **advisory** roll-up of a gated `/pharn-dev-ship` run. It records that the ## Increment -Make the pharn-cli installer **layout-aware**: mirror whichever layout the fetched pharn-oss clone has — the new `pharn/` single-install layout (PR #86 / `pharn-runtime-layout`) OR the legacy flat layout — across all archetype surfaces (install, status/drift, remove), recording the installed layout in `pharn.config.json`. This is the layout half of the original `/pharn-dev-ship` request; the DOCS_URL half already shipped as `remove-dead-docs-url`. +Make the pharn installer **layout-aware**: mirror whichever layout the fetched pharn-oss clone has — the new `pharn/` single-install layout (PR #86 / `pharn-runtime-layout`) OR the legacy flat layout — across all archetype surfaces (install, status/drift, remove), recording the installed layout in `pharn.config.json`. This is the layout half of the original `/pharn-dev-ship` request; the DOCS_URL half already shipped as `remove-dead-docs-url`. **GATE-1 decisions (human):** all-at-once (write + read side together); clone ref stays `main`; mirror PR #86's current subtree paths (accepted caveat: a follow-up tweak if #86 renames a subtree before merge). diff --git a/.dev/features/list-archetype-aware/PLAN.md b/.dev/features/list-archetype-aware/PLAN.md index de65eac..d86d302 100644 --- a/.dev/features/list-archetype-aware/PLAN.md +++ b/.dev/features/list-archetype-aware/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 — sha256 of ARCHITECTURE.md, read this run - increment: Add a shared `isArchetypeConfig` discriminator and make `pharn list` render an archetype install's capabilities/archetypes **offline** (no manifest fetch) instead of crashing — **slice 1 of 5** toward full sibling-command archetype parity. -- layer(s): **product** (`pharn-cli` CLI, `src/`). +- layer(s): **product** (`pharn` CLI, `src/`). - constitution_refs: [P3, P4, P5, P6, P7] ## Context (this run, live reads) diff --git a/.dev/features/model-routing-config/PLAN.md b/.dev/features/model-routing-config/PLAN.md index 53dea24..baf4be1 100644 --- a/.dev/features/model-routing-config/PLAN.md +++ b/.dev/features/model-routing-config/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 — sha256(ARCHITECTURE.md), read this run - increment: Add a `models` block to the `pharn.config.json` schema (per-stage `{model, effort}` with a `default` fallback), a deterministic **validator** (reject bad model / effort / stage key) and a pure **resolver** (`stage → {model,effort}`, missing → default), and write the block with sensible defaults on `init`. -- layer(s): **product** (`pharn-cli` CLI, `src/`) — NOT a `pharn-*` methodology Capability and NOT an ARCHITECTURE §4 methodology layer. This is installer schema + logic; the CLI owns the `pharn.config.json` schema (P3, CLAUDE.md). Its floor is `npm run check`, not `.dev/floor/validate.mjs` (which walks only `.md` capabilities and excludes `src/`). +- layer(s): **product** (`pharn` CLI, `src/`) — NOT a `pharn-*` methodology Capability and NOT an ARCHITECTURE §4 methodology layer. This is installer schema + logic; the CLI owns the `pharn.config.json` schema (P3, CLAUDE.md). Its floor is `npm run check`, not `.dev/floor/validate.mjs` (which walks only `.md` capabilities and excludes `src/`). - constitution_refs: [P0, P1, P3, P5, P6, P7] ## What this increment IS (and is NOT) @@ -27,7 +27,7 @@ ## Naming-collision note (surfaced, NOT resolved by me — trusted docs are hook-protected) -`ARCHITECTURE.md §3.1` already defines `model_tier ∈ {haiku, sonnet, opus}` as **advisory metadata on a pharn-oss Capability's frontmatter**. That is a **different concept** from this `models` block (versioned model **ids** + an **effort** level, in `pharn.config.json`, owned by pharn-cli). This increment does **not** touch, redefine, or edit `§3.1` / `model_tier` (ARCHITECTURE.md is trusted + hook-protected; I will not edit it). The two coexist on different axes (P3). Named for a human, not merged. +`ARCHITECTURE.md §3.1` already defines `model_tier ∈ {haiku, sonnet, opus}` as **advisory metadata on a pharn-oss Capability's frontmatter**. That is a **different concept** from this `models` block (versioned model **ids** + an **effort** level, in `pharn.config.json`, owned by pharn). This increment does **not** touch, redefine, or edit `§3.1` / `model_tier` (ARCHITECTURE.md is trusted + hook-protected; I will not edit it). The two coexist on different axes (P3). Named for a human, not merged. ## Files @@ -69,7 +69,7 @@ ## Contracts satisfied -- **None in `pharn-contracts/`** — it holds only `eval-format.md`, `finding-shape.md`, `seam-config.md`, none of which govern `pharn.config.json`. **pharn-cli owns the `pharn.config.json` schema** (P3, CLAUDE.md); no methodology contract applies. Stated, not fabricated (P4). +- **None in `pharn-contracts/`** — it holds only `eval-format.md`, `finding-shape.md`, `seam-config.md`, none of which govern `pharn.config.json`. **pharn owns the `pharn.config.json` schema** (P3, CLAUDE.md); no methodology contract applies. Stated, not fabricated (P4). ## Evals to write (P1 — for product TS, the vitest suite IS the spec) diff --git a/.dev/features/npm-publish-metadata/GRILL.md b/.dev/features/npm-publish-metadata/GRILL.md index a67f7e8..5fa0b59 100644 --- a/.dev/features/npm-publish-metadata/GRILL.md +++ b/.dev/features/npm-publish-metadata/GRILL.md @@ -48,7 +48,7 @@ _Not strictly blocking_ (the `## Decisions` block does resolve all five, human-a severity: minor file: ".dev/features/npm-publish-metadata/PLAN.md:24" problem: "`repository.url` is asserted 'EXACT' but no check verifies it matches the building repo's actual git remote. A drift here does not fail locally — it fails provenance attestation late, in the PR2 publish CI." - evidence: "`repository`: `{ \"type\": \"git\", \"url\": \"git+https://github.com/pharn-dev/pharn-cli.git\" }` (EXACT — provenance attestation validates this against the building repo)" + evidence: "`repository`: `{ \"type\": \"git\", \"url\": \"git+https://github.com/pharn-dev/pharn.git\" }` (EXACT — provenance attestation validates this against the building repo)" ``` ## Prose summary (concerns for the human/builder to weigh) @@ -59,7 +59,7 @@ The plan is well-scoped and its guarantee audit is honest (the provenance claim 2. **Name the deterministic checks (minor).** Two load-bearing claims — "no `src/` changed" and "zero junk in the tarball" — should each cite the concrete command that verifies them (`git diff --name-only` shows no `src/**`; the `npm pack --dry-run` file list is inspected). Building will run exactly these, so this is a wording tightening, not new work. 3. **Provenance precondition (minor).** Add a build-time confirmation that the declared `repository.url` matches `git remote get-url origin`, so a wrong URL fails now, not in the PR2 publish. -**Out of scope for this increment (dev-tooling observation, not a plan defect):** `count-grillers .` (and by extension the verify-stage counters) register 81 capabilities from gitignored `test-*/` scratch installs — the pharn-cli repo's own dev-loop has none. The discovery scan should probably exclude gitignored/scratch trees. Flagged for the human; unrelated to publishing the package. +**Out of scope for this increment (dev-tooling observation, not a plan defect):** `count-grillers .` (and by extension the verify-stage counters) register 81 capabilities from gitignored `test-*/` scratch installs — the pharn repo's own dev-loop has none. The discovery scan should probably exclude gitignored/scratch trees. Flagged for the human; unrelated to publishing the package. ## Verdict diff --git a/.dev/features/npm-publish-metadata/PLAN.md b/.dev/features/npm-publish-metadata/PLAN.md index 44b2ab2..182de13 100644 --- a/.dev/features/npm-publish-metadata/PLAN.md +++ b/.dev/features/npm-publish-metadata/PLAN.md @@ -7,10 +7,10 @@ ## Files -- `package.json` — npm publish metadata (see concrete values below): `name` pharn-cli→**pharn**, `description`, `keywords`, `repository`/`bugs`/`homepage`, `bin`→`{pharn}`, `publishConfig{access,provenance}`, `scripts.prepublishOnly` + `scripts.prepack`, `engines` (decision Q1). `version` **stays 0.2.0** (metadata, not behavior). — layer: repo packaging -- `README.md` — add an **Install** section (`npx pharn@latest init`); update the npm badge `pharn-cli`→`pharn` and the "npm package is `pharn-cli` / both bins" note (line ~43) to the new name + single bin. **No** module-prose rewrite (see Q4). — layer: docs +- `package.json` — npm publish metadata (see concrete values below): `name` pharn→**pharn**, `description`, `keywords`, `repository`/`bugs`/`homepage`, `bin`→`{pharn}`, `publishConfig{access,provenance}`, `scripts.prepublishOnly` + `scripts.prepack`, `engines` (decision Q1). `version` **stays 0.2.0** (metadata, not behavior). — layer: repo packaging +- `README.md` — add an **Install** section (`npx pharn@latest init`); update the npm badge `pharn`→`pharn` and the "npm package is `pharn` / both bins" note (line ~43) to the new name + single bin. **No** module-prose rewrite (see Q4). — layer: docs - `CHANGELOG.md` — one `[Unreleased]` entry: rename to `pharn` + publish-readiness metadata, Keep-a-Changelog format. — layer: docs -- `CLAUDE.md` — update the "Published as `pharn-cli`, exposing both `pharn` and `pharn-cli` bins" line to the new `pharn` package name + single bin. — layer: project guidance +- `CLAUDE.md` — update the "Published as `pharn`, exposing both `pharn` and `pharn` bins" line to the new `pharn` package name + single bin. — layer: project guidance Out of scope for this increment (no `src/` change; no `.github/workflows/` change; no `pharn.config.json` schema change; the trusted docs are write-protected and untouched). @@ -21,10 +21,10 @@ Out of scope for this increment (no `src/` change; no `.github/workflows/` chang - `description`: `"Audit-grade AI development methodology for Claude Code — spec, plan, grill, build, verify, ship."` (also de-stales the current modules/stack-pack wording) - `keywords`: `["claude-code","ai","methodology","code-review","audit","agents"]` - `license`: `"Apache-2.0"` (already set — no change) -- `repository`: `{ "type": "git", "url": "git+https://github.com/pharn-dev/pharn-cli.git" }` (EXACT — provenance attestation validates this against the building repo) -- `bugs`: `{ "url": "https://github.com/pharn-dev/pharn-cli/issues" }` -- `homepage`: `"https://github.com/pharn-dev/pharn-cli#readme"` -- `bin`: `{ "pharn": "dist/index.js" }` (Q2 — drop the `pharn-cli` alias) +- `repository`: `{ "type": "git", "url": "git+https://github.com/pharn-dev/pharn.git" }` (EXACT — provenance attestation validates this against the building repo) +- `bugs`: `{ "url": "https://github.com/pharn-dev/pharn/issues" }` +- `homepage`: `"https://github.com/pharn-dev/pharn#readme"` +- `bin`: `{ "pharn": "dist/index.js" }` (Q2 — drop the `pharn` alias) - `files`: `["dist"]` (UNCHANGED — verified sufficient: runtime is `dist/**` only; the CLI fetches pharn-oss via `degit`/`fetch` at runtime and bundles no templates; the only package-relative read is `require('../package.json')`, always included by npm) - `engines`: `{ "node": ">=20" }` (Q1 — recommend keep 20, not the task's 18) - `publishConfig`: `{ "access": "public", "provenance": true }` @@ -60,7 +60,7 @@ Out of scope for this increment (no `src/` change; no `.github/workflows/` chang **Status: RESOLVED — none open.** All five below were resolved by human decision at GATE 1 (see `## Decisions` below) and are retained only for the audit trail. There is **no** unresolved question blocking `/pharn-dev-build`. 1. **`engines`** — keep `>=20` (**recommended**: the already-declared, CI-tested baseline; global `fetch`/`AbortController` were experimental before Node 21) **or** set the task's `>=18` (an untested compatibility claim)? -2. **`bin`** — single `{ "pharn": "dist/index.js" }` (**recommended**: the package *is* `pharn` now; a `pharn-cli` bin on a package named `pharn` is confusing) **or** keep the `pharn-cli` alias too? +2. **`bin`** — single `{ "pharn": "dist/index.js" }` (**recommended**: the package *is* `pharn` now; a `pharn` bin on a package named `pharn` is confusing) **or** keep the `pharn` alias too? 3. **Increment split** — confirm **PR1 = npm metadata (this run)** and **PR2 = OIDC release workflow (a separate `/pharn-dev-ship` run)**? release.yml depends on this PR's `prepack`, so PR1 must land first. 4. **README module-model staleness (P4)** — the README still describes the **removed** module/manifest system (module table, "pick which modules and stack pack", `orm:prisma`, privacy-posture wizard). Leave it and file a **separate fast-follow** (**recommended** — keeps this PR tight; my README edits introduce no new P4 violation) **or** expand PR1 to de-stale it now? 5. **`prepack: npm run build`** — confirm adding it. It is **not** in the literal task spec but is **required** for a working publish (Q-rationale in the guarantee audit). Recommend: **yes**. @@ -68,7 +68,7 @@ Out of scope for this increment (no `src/` change; no `.github/workflows/` chang ## Decisions (resolved at GATE 1 — 2026-07-22, human-approved) - Q1 `engines` → **keep `>=20`** (not `>=18`). -- Q2 `bin` → **single `{ "pharn": "dist/index.js" }`** (drop the `pharn-cli` alias). +- Q2 `bin` → **single `{ "pharn": "dist/index.js" }`** (drop the `pharn` alias). - Q3 increment split → **confirmed**: this run = PR1 (npm metadata); PR2 (OIDC `release.yml`) is a separate `/pharn-dev-ship` run. - Q4 README staleness → **fast-follow**; PR1 stays tight (Install section + badge/bin note only). - Q5 `prepack: npm run build` → **yes**, add it. diff --git a/.dev/features/npm-publish-metadata/REGRESSION.md b/.dev/features/npm-publish-metadata/REGRESSION.md index f926b93..d168912 100644 --- a/.dev/features/npm-publish-metadata/REGRESSION.md +++ b/.dev/features/npm-publish-metadata/REGRESSION.md @@ -32,7 +32,7 @@ Identical. My increment changed **no** `src/`, `tests/`, floor, hook, or capabil ## The CLI's real suite (named residual, P0/P7) -`/pharn-dev-regress`'s `--tests` glob (`*.test.mjs`/`*.test.cjs`) targets the **methodology floor** tests, not the pharn-cli **vitest** suite (`tests/*.test.ts`, run by `npm run check`). That vitest suite is this increment's true regression surface. It is **byte-identical at base and head** — the diff touches no `src/` or `tests/` file — so a flip is provably impossible (the same reasoning `/pharn-dev-regress` uses to skip style gates over byte-identical files). It was also run directly at HEAD during build: **`npm run check` GREEN, 378/378 tests pass.** +`/pharn-dev-regress`'s `--tests` glob (`*.test.mjs`/`*.test.cjs`) targets the **methodology floor** tests, not the pharn **vitest** suite (`tests/*.test.ts`, run by `npm run check`). That vitest suite is this increment's true regression surface. It is **byte-identical at base and head** — the diff touches no `src/` or `tests/` file — so a flip is provably impossible (the same reasoning `/pharn-dev-regress` uses to skip style gates over byte-identical files). It was also run directly at HEAD during build: **`npm run check` GREEN, 378/378 tests pass.** ## Verdict diff --git a/.dev/features/npm-publish-metadata/SHIP.md b/.dev/features/npm-publish-metadata/SHIP.md index de1e0eb..c6cdc74 100644 --- a/.dev/features/npm-publish-metadata/SHIP.md +++ b/.dev/features/npm-publish-metadata/SHIP.md @@ -18,7 +18,7 @@ Advisory stages (no structural verdict): `/pharn-dev-grill` → `GRILL.md` (4 co ## What landed (git diff — 4 files, no `src/`) -- `package.json` — `name` pharn-cli→**pharn**; `description`/`keywords`/`repository`/`bugs`/`homepage`; single `pharn` bin; `publishConfig{access:public, provenance:true}`; `scripts.prepack "npm run build"` + `scripts.prepublishOnly "npm run check"`; **`build` → clean-then-`tsc`**; `engines` kept `>=20`. +- `package.json` — `name` pharn→**pharn**; `description`/`keywords`/`repository`/`bugs`/`homepage`; single `pharn` bin; `publishConfig{access:public, provenance:true}`; `scripts.prepack "npm run build"` + `scripts.prepublishOnly "npm run check"`; **`build` → clean-then-`tsc`**; `engines` kept `>=20`. - `README.md` — Install section (`npx pharn@latest init`); npm badge → `pharn`; single-bin note. - `CHANGELOG.md` — `[Unreleased]` rename entry. - `CLAUDE.md` — rename note (single `pharn` bin). diff --git a/.dev/features/npm-publish-metadata/VERIFY.md b/.dev/features/npm-publish-metadata/VERIFY.md index acfadf4..e516cbb 100644 --- a/.dev/features/npm-publish-metadata/VERIFY.md +++ b/.dev/features/npm-publish-metadata/VERIFY.md @@ -19,7 +19,7 @@ Feature: `npm-publish-metadata` (rename package to `pharn` + publish metadata; 4 `node .dev/floor/validate.mjs .` returns **two different values** here, and I used the tracked-repo one — with the reason stated, not hidden: - **Working tree: exit 1 (RED).** `validate.mjs .` walks the whole tree and finds intentional red fixtures inside the gitignored `test-*/` scratch installs (`test-next/pharn/floor/test-fixtures/red/skill.md`, `test-spa/...`). These are **untracked local scratch**, not part of the repo. -- **Clean checkout: exit 0 (GREEN).** In a fresh `git worktree` of HEAD — i.e. exactly what CI's `.github/workflows/floor.yml` runs on `actions/checkout` — validate reports `FLOOR: GREEN — 0 capabilities checked in .` The tracked pharn-cli repo ships **zero markdown capabilities** (per `pharn-dev-build.md`: "pharn-cli is TypeScript … the structural `validate.mjs` floor stays vacuously-green until a markdown capability is actually added"). +- **Clean checkout: exit 0 (GREEN).** In a fresh `git worktree` of HEAD — i.e. exactly what CI's `.github/workflows/floor.yml` runs on `actions/checkout` — validate reports `FLOOR: GREEN — 0 capabilities checked in .` The tracked pharn repo ships **zero markdown capabilities** (per `pharn-dev-build.md`: "pharn is TypeScript … the structural `validate.mjs` floor stays vacuously-green until a markdown capability is actually added"). The verdict uses the **clean/tracked** value (0) because verify asks "is *the repo* green with this feature in it," and the repo = tracked content = what CI and `npm publish` see. The untracked scratch is neither committed, nor in CI, nor in the tarball. This increment adds no capability, so it cannot change either value. **This is a real captured value from a clean worktree, not an assertion** — and it matches CI floor.yml exactly. diff --git a/.dev/features/regress/PLAN.md b/.dev/features/regress/PLAN.md index f372656..e03cce9 100644 --- a/.dev/features/regress/PLAN.md +++ b/.dev/features/regress/PLAN.md @@ -15,7 +15,7 @@ - `ARCHITECTURE.md:199` + `:122` — pipeline spine `spec → plan → grill → build → regress → verify → ship` (also `README.md`, `CLAUDE.md:148`). - `ARCHITECTURE.md:208` — the stage's typed-artifact row: `| regress | regression-report | regressions outside the feature |`. This plan's `regression-report.json` (machine) + `REGRESSION.md` (human) **are** that regression-report (see "Spec alignment"). - **Live git/state grounding (P6), shapes the baseline rule below:** - - Working tree is **clean**, on `main`, `HEAD = 7204a5f`. `origin/main` exists (`git@github.com:pharn-dev/pharn-cli.git`). Features are built on **branches** (`attempt-0-trust-fence`, `feature/grill-command`, …) then merged to `main` — so at `/regress` time the base is normally `merge-base(HEAD, origin/main)`; in a working-tree dogfood build (uncommitted changes on `main`) the base is `HEAD`. + - Working tree is **clean**, on `main`, `HEAD = 7204a5f`. `origin/main` exists (`git@github.com:pharn-dev/pharn.git`). Features are built on **branches** (`attempt-0-trust-fence`, `feature/grill-command`, …) then merged to `main` — so at `/regress` time the base is normally `merge-base(HEAD, origin/main)`; in a working-tree dogfood build (uncommitted changes on `main`) the base is `HEAD`. - The **one committed eval fixture pair** the suite can re-check: expected `pharn-review/trust-fence/evals/expected/expected-injection-comment.json` ↔ committed actual `features/trust-fence/findings.json` (verified live). - The **core** deterministic suite (`node --test …`, `node floor/validate.mjs .`, `node floor/check-structural.mjs …`) runs **without `npm ci`** (Node stdlib only). The **style** gates (`eslint` / `prettier` / `markdownlint-cli2`) need devDeps — relevant to the baseline-worktree cost named below. - **No `regress.md`, no `floor/check-regress*.mjs`, and no `features/regress/` existed before this run** (clean slate; this plan is the first file). diff --git a/.dev/features/remove-dead-docs-url/GRILL.md b/.dev/features/remove-dead-docs-url/GRILL.md index 20ebdc7..7480aff 100644 --- a/.dev/features/remove-dead-docs-url/GRILL.md +++ b/.dev/features/remove-dead-docs-url/GRILL.md @@ -3,7 +3,7 @@ Plan under interrogation: `.dev/features/remove-dead-docs-url/PLAN.md` (trust: untrusted to this griller). Spec-hash check: recomputed `sha256(ARCHITECTURE.md)` = `bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e` — **matches** the plan's `spec_content_hash` (no drift; `/pharn-dev-build`'s fix #4 gate is the actual block). -Griller discovery (`.dev/floor/count-grillers.mjs .`, membership FLOOR): 13 `role: griller` files registered, **all under `test-app/`** — the installed test fixture (the CLI's product *output*), not this dev repo's own methodology. None is a dev-loop griller for a pharn-cli plan, so the pluggable slot runs none over this plan. The one relevant axis — **testability** — is applied inline below from `test-app/…/testability/testability.md` (the only copy present), since the plan makes an explicit "no new test" claim. +Griller discovery (`.dev/floor/count-grillers.mjs .`, membership FLOOR): 13 `role: griller` files registered, **all under `test-app/`** — the installed test fixture (the CLI's product *output*), not this dev repo's own methodology. None is a dev-loop griller for a pharn plan, so the pluggable slot runs none over this plan. The one relevant axis — **testability** — is applied inline below from `test-app/…/testability/testability.md` (the only copy present), since the plan makes an explicit "no new test" claim. ## Findings — by axis diff --git a/.dev/features/remove-dead-docs-url/PLAN.md b/.dev/features/remove-dead-docs-url/PLAN.md index d6f2e50..b5c0364 100644 --- a/.dev/features/remove-dead-docs-url/PLAN.md +++ b/.dev/features/remove-dead-docs-url/PLAN.md @@ -2,7 +2,7 @@ - spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 (ARCHITECTURE.md) - increment: Remove the dead `DOCS_URL` constant and its two post-install "Docs" output lines — no replacement URL (human decision: "just remove the URL"). -- layer(s): pharn-cli product source — `src/lib/` (constants) + `src/steps/` (two install stages). ARCHITECTURE.md §4. +- layer(s): pharn product source — `src/lib/` (constants) + `src/steps/` (two install stages). ARCHITECTURE.md §4. - constitution_refs: [P4, P7, P1] ## Files diff --git a/.dev/features/remove-dead-docs-url/REGRESSION.md b/.dev/features/remove-dead-docs-url/REGRESSION.md index 62f04e5..b6cae7d 100644 --- a/.dev/features/remove-dead-docs-url/REGRESSION.md +++ b/.dev/features/remove-dead-docs-url/REGRESSION.md @@ -8,7 +8,7 @@ Pure exit-code comparison of the outside gate set at the baseline and at HEAD, z - **base:** `b680a99` (`b680a990d25b54ca8096aa2e64c56468e1b464b6`) — current HEAD. Working tree is dirty (the feature's 3 uncommitted src edits), so per the deterministic base rule `base = HEAD` and the diff is taken against the working tree. - **inside (declared `## Files`, 3):** `src/lib/constants.ts`, `src/steps/install.ts`, `src/steps/install-archetype.ts`. `scope` exit `0`, `escaped: []` — the build stayed within its declared writes (fix #7). -- **outside gate set:** `tests` (44 stdlib `node --test` floor + hook files) and `validate`. Style gates skipped — `inside` touches no shared style config (`eslint.config.mjs`, `.prettierrc.json`, `.prettierignore`, `.markdownlint-cli2.jsonc`). No outside eval pairs (`pharn-cli` ships no committed lens/griller eval pairs at repo root). +- **outside gate set:** `tests` (44 stdlib `node --test` floor + hook files) and `validate`. Style gates skipped — `inside` touches no shared style config (`eslint.config.mjs`, `.prettierrc.json`, `.prettierignore`, `.markdownlint-cli2.jsonc`). No outside eval pairs (`pharn` ships no committed lens/griller eval pairs at repo root). ## Per-gate base → head (exit codes) @@ -21,7 +21,7 @@ Pure exit-code comparison of the outside gate set at the baseline and at HEAD, z ## Why the `tests` gate is RED at baseline (not this feature) -The `tests` aggregate is red **independently of this increment** — it is red with my edits and red with them reverted. The failing file is `.dev/floor/lens-scanner-map.test.mjs`, which is **explicitly non-hermetic** (its own header: "UNLIKE the other floor tests … this one validates the COMMITTED artifact against REALITY"): it runs `count-lenses.mjs` over the **repo root**, which recursively includes the gitignored `test-app/` install fixture. `count-lenses .` returns **22 lenses, all under `test-app/pharn-review/…`** (copy-paste-drift, duplicated-logic, injection, …), while the committed `lens-scanner-map.json` is intentionally empty (`"scanners": {}` — "pharn-cli ships no product lenses today"). 22 ≠ 0 ⇒ the test asserts, and `node --test` exits non-zero. +The `tests` aggregate is red **independently of this increment** — it is red with my edits and red with them reverted. The failing file is `.dev/floor/lens-scanner-map.test.mjs`, which is **explicitly non-hermetic** (its own header: "UNLIKE the other floor tests … this one validates the COMMITTED artifact against REALITY"): it runs `count-lenses.mjs` over the **repo root**, which recursively includes the gitignored `test-app/` install fixture. `count-lenses .` returns **22 lenses, all under `test-app/pharn-review/…`** (copy-paste-drift, duplicated-logic, injection, …), while the committed `lens-scanner-map.json` is intentionally empty (`"scanners": {}` — "pharn ships no product lenses today"). 22 ≠ 0 ⇒ the test asserts, and `node --test` exits non-zero. - **This has nothing to do with `DOCS_URL`.** No `node --test` file imports or reads any of my 3 changed `src/*.ts` files (only the vitest `tests/*.test.ts` suite does — and that ran GREEN, 594/594, at build). Removing `DOCS_URL` cannot move the lens count. - **A first (worktree) capture read 0→1 — a measurement artifact, corrected.** The command's `git worktree` baseline checks out only tracked files, so the gitignored `test-app/` is absent there → `count-lenses` finds 0 → the lens test passes → `tests` = 0 at that base, while HEAD (working tree, `test-app` present) = 1. That 0→1 is an **apples-to-oranges** difference in *environment* (fixture present/absent), not a feature effect, so it was discarded. diff --git a/.dev/features/remove-vendor-skill/PLAN.md b/.dev/features/remove-vendor-skill/PLAN.md index 6e64375..80fae17 100644 --- a/.dev/features/remove-vendor-skill/PLAN.md +++ b/.dev/features/remove-vendor-skill/PLAN.md @@ -74,7 +74,7 @@ symbols are cross-cutting, so a partial removal leaves dangling references and t - The CLI's own `pharn.config.json` schema ownership (CLAUDE.md, "this CLI owns the config schema"): the schema stays **additive** — a legacy config carrying a now-unused `vendorSkills` key still loads (`readPharnConfig` is a passthrough; P7). Cite, don't restate. (No `pharn-contracts/` contract is - implemented — this is a removal within pharn-cli.) + implemented — this is a removal within pharn.) ## Evals to write (P1 — the updated tests ARE the new spec) diff --git a/.dev/features/remove-vendor-skill/REGRESSION.md b/.dev/features/remove-vendor-skill/REGRESSION.md index 77dfe3d..168d47b 100644 --- a/.dev/features/remove-vendor-skill/REGRESSION.md +++ b/.dev/features/remove-vendor-skill/REGRESSION.md @@ -6,7 +6,7 @@ ## Inside / outside partition (deterministic — `check-regress.mjs scope`, exit 0) - **Inside (21 changed files ⊆ declared `## Files`):** `src/{types, lib/{manifest,wizard,validate,vendor-fetch}, commands/init, steps/{install,summary,vendor-consent}}`, the 9 touched `tests/*.ts`, the 3 `docs/*.md`. **`escaped: []`** — no fix#7 scope breach; the build stayed inside the plan's `## Files`. -- **Outside gates run:** 44 floor/hook `*.test.mjs` + `*.test.cjs` (the `tests` gate), whole-repo `validate`, and the pharn-cli `vitest` suite. **0 outside eval pairs.** +- **Outside gates run:** 44 floor/hook `*.test.mjs` + `*.test.cjs` (the `tests` gate), whole-repo `validate`, and the pharn `vitest` suite. **0 outside eval pairs.** - **Style gates (`lint`/`format:check`/`lint:md`) skipped** by the deterministic config-touch rule: `inside` touches no shared style config (`eslint.config.mjs`, `.prettierrc`, `.prettierignore`, `.markdownlint-cli2.jsonc`), so a style flip over the byte-identical outside files is provably impossible. ## Per-gate exit codes: base → head @@ -18,7 +18,7 @@ | `vitest` | 0 | 0 | no | - `tests`: `node --test` over the 44 floor/hook test files — **663 pass** both sides (unchanged by this src-only feature). -- `vitest`: the pharn-cli suite — **509 pass at base** (old suite, incl. the vendor tests) → **496 pass at head** (net −13: the removed vendor tests, plus the 2 added regression tests). The test *count* changed by design; the **gate did not flip pass→fail**, which is what a regression is. +- `vitest`: the pharn suite — **509 pass at base** (old suite, incl. the vendor tests) → **496 pass at head** (net −13: the removed vendor tests, plus the 2 added regression tests). The test *count* changed by design; the **gate did not flip pass→fail**, which is what a regression is. ## `regressions[]`: none · `pre_existing[]`: none @@ -27,10 +27,10 @@ ## Honest residual (P0/P7 — what this catches, and what it does not) `/pharn-dev-regress` catches exactly what its suite catches, nothing more. Here that suite is the 44 floor/hook -`.test.mjs`/`.test.cjs` gates + `validate` + the pharn-cli `vitest` suite. The `vitest` gate is the one +`.test.mjs`/`.test.cjs` gates + `validate` + the pharn `vitest` suite. The `vitest` gate is the one that actually exercises this feature's blast radius: the shared `lib/{manifest,wizard,validate}` + `types.ts` edits are consumed by `add`/`remove`/`update`/`list`/`status`, whose **unchanged** spec files (`add.test.ts`, `remove.test.ts`, `update.test.ts`, `list.test.ts`, `status.test.ts`, `installer.test.ts`, -`install-modules.test.ts`, `manifest.test.ts`, …) all pass at head — so no outside pharn-cli behavior +`install-modules.test.ts`, `manifest.test.ts`, …) all pass at head — so no outside pharn behavior regressed. A breakage that no deterministic check covers would be invisible; this verdict is "deterministically-detectable breakage outside the feature is caught," not "nothing broke." diff --git a/.dev/floor/lens-scanner-map.json b/.dev/floor/lens-scanner-map.json index cd608e5..3489bba 100644 --- a/.dev/floor/lens-scanner-map.json +++ b/.dev/floor/lens-scanner-map.json @@ -1,4 +1,4 @@ { - "doc": "Explicit lens -> code-scanner map for the parallel /pharn-review scanner-prefilter. The orchestrator runs a lens's mapped scanner over the review target; files with a hit are that lens's scoped slice (its subagent reads only those). A null value = the lens is SCANNER-LESS: no deterministic prefilter exists, so its subagent falls back to the whole target (an honestly-labeled advisory bound). This map is the MACHINE-READABLE projection of the binding each lens also names in its Layer-1 prose; it is a convenience/consistency artifact for the advisory orchestrator, NOT a new source of truth for what a lens does (P4). It exists EXPLICITLY (not name-derived) because the lens->scanner names do not match by convention (insecure-crypto->scan-code-crypto, unsafe-deserialization->scan-code-deserialization, secrets-in-code->scan-code-secrets, placeholder-as-done->scan-code-placeholder) and 4 lenses name no usable scanner. lens-scanner-map.test.mjs enforces consistency with disk + count-lenses so prose/map drift is caught (P7 — a real, already-observed drift: two lenses' prose name scanners that do not exist). pharn-cli ships no product lenses today — the map is empty until pharn-review capabilities are added.", + "doc": "Explicit lens -> code-scanner map for the parallel /pharn-review scanner-prefilter. The orchestrator runs a lens's mapped scanner over the review target; files with a hit are that lens's scoped slice (its subagent reads only those). A null value = the lens is SCANNER-LESS: no deterministic prefilter exists, so its subagent falls back to the whole target (an honestly-labeled advisory bound). This map is the MACHINE-READABLE projection of the binding each lens also names in its Layer-1 prose; it is a convenience/consistency artifact for the advisory orchestrator, NOT a new source of truth for what a lens does (P4). It exists EXPLICITLY (not name-derived) because the lens->scanner names do not match by convention (insecure-crypto->scan-code-crypto, unsafe-deserialization->scan-code-deserialization, secrets-in-code->scan-code-secrets, placeholder-as-done->scan-code-placeholder) and 4 lenses name no usable scanner. lens-scanner-map.test.mjs enforces consistency with disk + count-lenses so prose/map drift is caught (P7 — a real, already-observed drift: two lenses' prose name scanners that do not exist). pharn ships no product lenses today — the map is empty until pharn-review capabilities are added.", "scanners": {} } diff --git a/.dev/floor/lens-scanner-map.test.mjs b/.dev/floor/lens-scanner-map.test.mjs index dcd541c..8876eb9 100644 --- a/.dev/floor/lens-scanner-map.test.mjs +++ b/.dev/floor/lens-scanner-map.test.mjs @@ -65,7 +65,7 @@ test("3. every map KEY is a real counted lens — no phantom entry", () => { test("4. no ORPHAN scanner — every scan-code-*.mjs on disk is wired to some lens", () => { const live = countedLensNames(); - if (live.length === 0) return; // pharn-cli ships floor scanners without product lenses — wiring is enforced once lenses exist + if (live.length === 0) return; // pharn ships floor scanners without product lenses — wiring is enforced once lenses exist const referenced = new Set(Object.values(MAP.scanners).filter(Boolean)); for (const s of scannersOnDisk()) { assert.ok(referenced.has(s), `scanner '${s}' exists on disk but no lens maps to it (unwired scanner)`); diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 801acfc..ead9e8e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ A clear description of the bug. - **Command**: - **Step / area**: -- **`pharn-cli` version** (`npx pharn --version`, or `pharnVersion` in `pharn.config.json`): +- **`pharn` version** (`npx pharn --version`, or `pharnVersion` in `pharn.config.json`): - **Skills version** (`.claude/SKILLS_VERSION`, or `skillsVersion` in `pharn.config.json`): ## Steps to reproduce diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3d192ee..827b9d4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: true contact_links: - name: Security vulnerability - url: https://github.com/pharn-dev/pharn-cli/security/advisories/new + url: https://github.com/pharn-dev/pharn/security/advisories/new about: Please report security issues privately — do not open a public issue. See SECURITY.md. - name: Documentation - url: https://github.com/pharn-dev/pharn-cli/tree/main/docs + url: https://github.com/pharn-dev/pharn/tree/main/docs about: Getting started, stack options, and command reference. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index a72692e..8ea5194 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -26,7 +26,7 @@ What you'd like the `pharn` CLI to do. ## Checks - [ ] I searched existing issues for an overlapping request. -- [ ] This belongs in `pharn-cli` (the bootstrapper/wizard), not in the PHARN skills repo. +- [ ] This belongs in `pharn` (the bootstrapper/wizard), not in the PHARN skills repo. - [ ] If this is stack scaffolding or `npm` package installation, I understand it is **v0.2 scope** (see the `TODO(v0.2)` markers and `docs/roadmap.md`). ## Alternatives considered diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ff3259b..05b3423 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ diff --git a/.pharn/writes-scope.json b/.pharn/writes-scope.json index ad38694..6f1b0d8 100644 --- a/.pharn/writes-scope.json +++ b/.pharn/writes-scope.json @@ -1,14 +1,10 @@ { "scope": [ - "scripts/build.mjs", - "package.json", - "CLAUDE.md", - ".github/workflows/ci.yml", - "src/version.ts", - "src/index.ts", - "src/lib/banner.ts", - "src/steps/install-archetype.ts" + "README.md", + "docs/commands/init.md", + "docs/getting-started.md", + "docs/contributing.md" ], - "set_by": "manual-adhoc (user-authorized in chat: esbuild bundle+minify for dist/; extended to fix a bundling-exposed package.json-path bug in src/)", - "set_at": "2026-07-22T00:00:00.000Z" + "set_by": "manual-adhoc (user-authorized in chat: update README.md + fix stale legacy-fallback / two-bin doc claims to the archetype model)", + "set_at": "2026-07-22T13:58:59.025Z" } diff --git a/CHANGELOG.md b/CHANGELOG.md index a310750..9c9767a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to `pharn-cli` are documented in this file. +All notable changes to `pharn` are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). @@ -32,9 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Renamed the npm package `pharn-cli` → `pharn`** and made it publish-ready — added `repository`, +- **Renamed the npm package `pharn` → `pharn`** and made it publish-ready — added `repository`, `bugs`, `homepage`, `keywords`, and `publishConfig` (public access + provenance); dropped the - `pharn-cli` bin alias for a single `pharn` bin; and added a `prepack` build so `npm publish` always + `pharn` bin alias for a single `pharn` bin; and added a `prepack` build so `npm publish` always ships a freshly compiled `dist/`. No CLI behavior change and `version` is unchanged; the package now installs via `npx pharn@latest init`. - Docs: surfaced the new optional `/pharn-spec` stage (intent capture before `/pharn-plan`) in @@ -92,8 +92,8 @@ Realigned the CLI with the current `pharn-dev/pharn-oss`, which is now a ## [0.1.0] — 2026-06-11 -Initial published release. `pharn-cli` bootstraps the PHARN stack into an existing -Next.js project. Exposes both `pharn` and `pharn-cli` bins. +Initial published release. `pharn` bootstraps the PHARN stack into an existing +Next.js project. Exposes both `pharn` and `pharn` bins. ### Added @@ -127,6 +127,6 @@ Next.js project. Exposes both `pharn` and `pharn-cli` bins. `pharn.config.json`. It does not yet install npm packages or scaffold the stack — that is planned for v0.2 (see `docs/roadmap.md` and the `TODO(v0.2)` markers). -[Unreleased]: https://github.com/pharn-dev/pharn-cli/compare/v0.2.0...HEAD -[0.2.0]: https://github.com/pharn-dev/pharn-cli/compare/v0.1.0...v0.2.0 -[0.1.0]: https://github.com/pharn-dev/pharn-cli/releases/tag/v0.1.0 +[Unreleased]: https://github.com/pharn-dev/pharn/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/pharn-dev/pharn/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/pharn-dev/pharn/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md index c18a8fc..1bdccfe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this is -`pharn-cli` is an interactive CLI that installs [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code — into an existing project (framework-agnostic). `pharn init` detects the project's **archetype(s)** (`ssr`/`backend`/`spa`/`lib`) and installs the applicable PHARN **capabilities** (grillers/lenses) from `pharn-dev/pharn-oss` via degit, copying them plus the fixed product surfaces into the mirrored layout (`.claude/` + `pharn/`) and writing `pharn.config.json`. No module catalog / `manifest.json` fetch. Published on npm as `pharn`, exposing a single `pharn` bin. Targets Claude Code today; Codex and Cursor are planned. +`pharn` is an interactive CLI that installs [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code — into an existing project (framework-agnostic). `pharn init` detects the project's **archetype(s)** (`ssr`/`backend`/`spa`/`lib`) and installs the applicable PHARN **capabilities** (grillers/lenses) from `pharn-dev/pharn-oss` via degit, copying them plus the fixed product surfaces into the mirrored layout (`.claude/` + `pharn/`) and writing `pharn.config.json`. No module catalog / `manifest.json` fetch. Published on npm as `pharn`, exposing a single `pharn` bin. Targets Claude Code today; Codex and Cursor are planned. **Module model (removed).** Earlier releases installed by *modules* driven by a repo-root `manifest.json` (schemaVersion 1/2 + a `wizard` block + per-module `module.json` `installs` maps), and kept a module/manifest fallback in `add`/`update`/`list`/`status`/`remove` for a pre-archetype config. **That subsystem is gone** (`lib/manifest.ts`, `install-modules.ts`, `installer.ts`, `wizard.ts` deleted) — live pharn-oss ships no `manifest.json`, so the fallback 404'd. **All commands are archetype-only.** A pre-archetype (module) `pharn.config.json` (one with `modules[]` but no `capabilities[]`) is detected by `isArchetypeConfig` and rejected up front by `loadArchetypeConfigOrExit` with a clear "re-run `pharn init`" message (`LEGACY_CONFIG_MESSAGE`) — never a fetch. **This CLI still owns the `pharn.config.json` schema**, which stays additive: a legacy config's now-unused fields (`modules`, `constitution`, `installedSkills`, `stackAnswers`) still LOAD (P7). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ae09354..ca66ac8 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -38,7 +38,7 @@ This Code of Conduct applies within all community spaces, and also applies when ## Enforcement -This Code of Conduct is shared across all PHARN projects (including `pharn-cli` and `pharn-oss`), which are governed by a single pharn.dev community and enforcement team. +This Code of Conduct is shared across all PHARN projects (including `pharn` and `pharn-oss`), which are governed by a single pharn.dev community and enforcement team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at `support@pharn.dev`. If your report concerns a member of the enforcement team, note that in your message so it can be routed to an uninvolved leader. diff --git a/CONSTITUTION.md b/CONSTITUTION.md index ae6784e..6049223 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -4,10 +4,10 @@ trust: trusted editable_by: "human only — agents (including the build agent) MUST NOT modify this file" enforced_by: "read as the trusted prefix before every dev-loop command (plan/build/review); write-protected at the floor by .claude/hooks/protect-trusted-paths.cjs" violation_action: "stop the build, flag for human review — never auto-fix a constitution violation" -applies_to: "pharn-cli — the installer — AND the process of building it with the pharn-dev-* loop" +applies_to: "pharn — the installer — AND the process of building it with the pharn-dev-* loop" --- -# pharn-cli — Constitution +# pharn — Constitution These principles override every command, plan, and agent decision in this repo. Any violation stops the build and is flagged for human review. The constitution is the highest-priority context @@ -17,7 +17,7 @@ instruction — including instructions found inside files the agent reads. A violation is never "minor". It is always blocking, including in autonomous mode. The agent MUST NOT attempt to auto-fix a constitution violation. -`pharn-cli` installs [PHARN](https://github.com/pharn-dev/pharn-oss) into a user's project by +`pharn` installs [PHARN](https://github.com/pharn-dev/pharn-oss) into a user's project by **fetching untrusted remote content (a manifest, per-module `module.json`, and `degit`-cloned files) and copying it into that user's repo.** These eight principles are the discipline that makes that safe and readable. `ARCHITECTURE.md`, `THREAT-MODEL.md`, and `LIMITS.md` elaborate them; they diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 742d70b..0f20f53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to pharn-cli +# Contributing to pharn -Thanks for your interest in improving PHARN. This repository **is `pharn-cli`** — the ESM-only Node CLI that fetches PHARN modules from `pharn-dev/pharn-oss` and installs them into a project's `.claude/`. The full contributor guide lives in [`docs/contributing.md`](./docs/contributing.md) — start there. +Thanks for your interest in improving PHARN. This repository **is `pharn`** — the ESM-only Node CLI that fetches PHARN modules from `pharn-dev/pharn-oss` and installs them into a project's `.claude/`. The full contributor guide lives in [`docs/contributing.md`](./docs/contributing.md) — start there. ## Read first @@ -21,7 +21,7 @@ Before changing anything, read these in order: ## The 30-second version 1. **Open an issue first** for any non-trivial change. PHARN is small-surface on purpose. -2. **Install**: `cd pharn-cli && npm install` (dev-only tooling; only `dist/` ships to npm). +2. **Install**: `cd pharn && npm install` (dev-only tooling; only `dist/` ships to npm). 3. **Run the gates before pushing** — all four must pass (this is exactly what CI runs): `npm run format:check` · `npm run lint` · `npm run typecheck` · `npm test` 4. **Branch**: `feat/…`, `fix/…`, or `docs/…`. diff --git a/LIMITS.md b/LIMITS.md index dc41944..e851290 100644 --- a/LIMITS.md +++ b/LIMITS.md @@ -2,10 +2,10 @@ file: "LIMITS.md" trust: trusted editable_by: "human only" -purpose: "What pharn-cli does NOT guarantee. Labels the irreducible limits and the residual honestly. Required by P0 and P7: a limit sold as a guarantee is the disease this repo exists to prevent." +purpose: "What pharn does NOT guarantee. Labels the irreducible limits and the residual honestly. Required by P0 and P7: a limit sold as a guarantee is the disease this repo exists to prevent." --- -# pharn-cli — Limits (what we do not guarantee) +# pharn — Limits (what we do not guarantee) > Per P0 and P7, this file is not a disclaimer footnote — it is first-class architecture. If a claim > elsewhere contradicts a limit named here, **the limit wins.** @@ -18,21 +18,21 @@ These cannot be reduced to the floor. They are **not bugs to fix** — they are overselling**. Each has a floor backstop that bounds its blast radius; none has a fix that makes it a guarantee. -### 1a. pharn-cli validates placement, not content +### 1a. pharn validates placement, not content The floor contains **where** fetched files land (`safeJoin`) and validates their **paths + schema** — it does **not** vet the **semantic content** of the PHARN methodology it copies. A `module.json` with perfectly safe paths can still install methodology whose body is whatever the upstream shipped. - **Struck claim:** "`pharn init` installed it, so the methodology is safe." -- **True statement:** pharn-cli guarantees the files landed in `.claude/` without escaping; content +- **True statement:** pharn guarantees the files landed in `.claude/` without escaping; content trust is **provenance** (`pharn-dev/pharn-oss`) + the **user's review**. - **Backstop (floor):** `INSTALL_PATH_RE` + `safeJoin` bound a hostile module to content **inside** `.claude/`, never an arbitrary-path write. ### 1b. Trust in the remote is provenance, not cryptographic -pharn-cli records a `commit` SHA (best-effort via the GitHub API) but fetches from a mutable remote +pharn records a `commit` SHA (best-effort via the GitHub API) but fetches from a mutable remote via `degit`; it stores **no signature and no per-file content-hash**. A compromised or MITM'd upstream serving valid-shaped content passes the structural floor. @@ -71,10 +71,10 @@ HEAD (not the pinned `commit`). An upstream **rename** can orphan a file (`remov ## 2. The residual (named, bounded, not zeroed) -pharn-cli validates **structure** (paths contained, schema known, fetch bounded) but cannot validate +pharn validates **structure** (paths contained, schema known, fetch bounded) but cannot validate the **semantic safety** of the methodology content it installs (`THREAT-MODEL.md §5`). "Installed cleanly" = "landed without escaping," **not** "safe or correct." Co-located: when the dev-loop -reviews the pharn-cli code it builds, a finding's free-text is untrusted data; the enum-gated split +reviews the pharn code it builds, a finding's free-text is untrusted data; the enum-gated split bounds it but does not zero it. This is the one place trust rests on **provenance + review**, not the floor. @@ -109,7 +109,7 @@ silently unsupported. ## 4. What "good" means here -Per P0, claiming pharn-cli is "proven safe" would be the exact disease this repo prevents. The honest +Per P0, claiming pharn is "proven safe" would be the exact disease this repo prevents. The honest standard: - Every _guarantee_ reduces to the floor (`ARCHITECTURE.md §2`) **or** is labeled `advisory`. diff --git a/README.md b/README.md index b909018..e35670a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@
-# pharn-cli +# pharn **Install PHARN into your project in one command.** -The interactive installer for [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code that keeps comprehension debt legible instead of silent. +The installer for [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code that keeps comprehension debt legible instead of silent. [![npm](https://img.shields.io/npm/v/pharn)](https://www.npmjs.com/package/pharn) -[![CI](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml) -[![CodeQL](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml) +[![CI](https://github.com/pharn-dev/pharn/actions/workflows/ci.yml/badge.svg)](https://github.com/pharn-dev/pharn/actions/workflows/ci.yml) +[![CodeQL](https://github.com/pharn-dev/pharn/actions/workflows/codeql.yml/badge.svg)](https://github.com/pharn-dev/pharn/actions/workflows/codeql.yml) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green)](./LICENSE) [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](./package.json) @@ -49,7 +49,7 @@ npx pharn@latest init Vibe-coding with an AI agent is fast — until the chat history scrolls away and takes the _understanding_ with it. That gap is **comprehension debt** ([coined by Addy Osmani](https://addyosmani.com/blog/comprehension-debt/)), and it compounds faster than any other kind. PHARN keeps a markdown-canonical record — spec, constitution, diff, audit trail — in your repo, readable and diffable. -`pharn-cli` is how you get it. Run `pharn init` in your project to pick which PHARN modules and stack pack you want; the CLI fetches them from `pharn-dev/pharn-oss`, copies them into `.claude/`, materializes your constitution + memory bank, and writes `pharn.config.json`. +`pharn` is how you get it. Run `pharn init` in your project; the CLI detects your project's **archetype(s)**, fetches the applicable PHARN **capabilities** from `pharn-dev/pharn-oss`, copies them plus the canonical constitution into the mirrored layout (`.claude/` + `pharn/`), and writes `pharn.config.json`. > The npm package is `pharn`; it installs a single `pharn` binary. @@ -57,17 +57,23 @@ Vibe-coding with an AI agent is fast — until the chat history scrolls away and ## What it installs -PHARN ships as **modules** (subfolders of the pharn-oss repo). `pharn-core` is required; everything else is optional and depends on it. +`pharn init` detects your project's **archetype(s)** and installs the PHARN **capabilities** that apply to them — nothing you didn't ask for. There is no module catalog and no `manifest.json`: capabilities are the install unit. -| Module | What you get | -| ------ | ------------ | -| `pharn-core` | Constitution, markdown memory bank, privacy-shield + constitution-guard hooks, base skills | -| `pharn-pipeline` | The spec → plan → grill → build → regress → verify → ship pipeline | -| `pharn-review` | `/pharn-review` with 13 context lenses + fingerprint ledger | -| `pharn-audits` | 8 standalone audits (privacy, security, a11y, supply-chain, …) | -| `pharn-stack-nextjs` | Next.js + Supabase + Better Auth + Drizzle stack pack (pulls in the React base) | +- **Archetype** — a closed set describing what your project _is_: `ssr`, `backend`, `spa`, or `lib` (the frameworkless base). Detection merges your `package.json` dependency **names** with a bounded, symlink-safe file-tree scan (names only, never file bodies). A project can match several (Next + Express → `ssr` + `backend`); a signal-less project resolves to `lib`. +- **Capability** — one **griller** (a pipeline auditor) or **lens** (a review lens). Each declares `applies: 'universal'` or a set of archetypes; it is **selected** when universal or when its `applies` intersects your detected archetypes, and **skipped** otherwise (with the reason shown). -The wizard also asks a **privacy-posture** question and writes the matching constitution variant (`gdpr-strict`, `standard`, or `minimal`) to `.claude/CONSTITUTION.md`. On newer manifests it additionally installs only the per-technology skills you answer for (e.g. your ORM, database, auth) — never the sibling options you didn't pick. +After a summary of what was selected vs. skipped and your confirmation, the CLI copies the selected capabilities plus the fixed product surfaces into the mirrored layout and writes `pharn.config.json`: + +| Artifact | What lands in your project | +| -------- | -------------------------- | +| `pharn-pipeline/grillers//`, `pharn-review//` | The selected grillers + lenses (flat layout, or the same under `pharn/`) | +| `.claude/commands/` | The `pharn-*` product slash commands | +| `.claude/hooks/` | The deterministic `.cjs` floor hooks | +| `pharn-contracts/`, `.dev/floor/` | Inter-layer schemas + the floor checkers the commands invoke | +| `CONSTITUTION.md` | The canonical PHARN constitution, copied verbatim | +| `pharn.config.json` | `skillsVersion`, commit SHA, detected archetypes, installed capabilities, and layout | + +An existing `.claude/settings.json` is **never** overwritten. To adjust the selection afterward, use [`pharn add`](docs/commands/add.md) / [`pharn remove`](docs/commands/remove.md). --- @@ -76,7 +82,7 @@ The wizard also asks a **privacy-posture** question and writes the matching cons Once installed, PHARN gives Claude Code a spine of typed stages — each links back to the spec: ```text -spec → plan → grill → build → regress → verify → ship +spec → plan → grill → build → regress → verify → review → ship ``` After `pharn init`, open Claude Code and run **`/pharn-plan`** to plan your first feature (or **`/pharn-spec`** first for a fuzzy or larger feature). @@ -87,12 +93,11 @@ After `pharn init`, open Claude Code and run **`/pharn-plan`** to plan your firs | Command | Description | | ------- | ----------- | -| `pharn init` | Interactive setup wizard (default) | -| `pharn add ` | Add a module to an existing PHARN project | -| `pharn add :` | Add one technology skill (e.g. `orm:prisma`) | -| `pharn remove ` | Remove a module or skill from this project | -| `pharn update` | Update installed modules to the latest skills version | -| `pharn list` | List installed and available modules/skills (`--json`) | +| `pharn init` | Detect archetypes and install the applicable capabilities (default) | +| `pharn add ` | Add one capability, e.g. `a11y` or `lens:n-plus-one` | +| `pharn remove ` | Remove an installed capability (no arg: pick one interactively) | +| `pharn update` | Re-fetch installed capabilities at the latest skills version | +| `pharn list` | List installed archetypes + capabilities (`--json`) | | `pharn status` | Show version + local-drift status (read-only; `--strict`, `--no-drift`) | | `pharn -h`, `--help` | Show help | | `pharn -v`, `--version` | Show version | @@ -101,10 +106,9 @@ After `pharn init`, open Claude Code and run **`/pharn-plan`** to plan your firs ## Prerequisites -- **Git** initialized in the project (required for every install) -- **Stack-pack packages** — only when you pick a pack that declares them (e.g. `pharn-stack-nextjs` requires `next` in `package.json`) +- **Git** initialized in the project — the only requirement, checked up front before detection. -See [Getting started](docs/getting-started.md) for the full flow and fresh-project warnings. +There is no stack-pack selection and no package prerequisite to satisfy. See [Getting started](docs/getting-started.md) for the full flow and fresh-project warnings. --- @@ -123,7 +127,7 @@ Full reference: **[docs/](docs/README.md)** ## Development ```bash -cd pharn-cli +cd pharn npm install npm run dev # e.g. npm run dev -- init npm run build @@ -138,7 +142,7 @@ npm run build:install-local # link pharn into the local test-app/ ## Security -All remote input (manifest, repo/branch/commit, module and skill paths) is validated against strict allowlists, checked for path escapes, and fetched with `redirect: 'error'`, an 8s timeout, and a 256KB body cap. Found a vulnerability? Please follow [`SECURITY.md`](SECURITY.md) rather than opening a public issue. +All remote input (repo/branch/commit, capability names and paths, and capability frontmatter) is validated against strict allowlists, checked for path escapes, and fetched with `redirect: 'error'`, an 8s timeout, and a 256KB body cap. Contents copied from the clone are never executed or parsed by the CLI. Found a vulnerability? Please follow [`SECURITY.md`](SECURITY.md) rather than opening a public issue. --- diff --git a/SECURITY.md b/SECURITY.md index 332ef0d..8abe9fa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,23 +1,23 @@ # Security Policy -PHARN is an audit-grade methodology — taking security seriously is part of the brand, not an afterthought. `pharn-cli` is the bootstrapper that fetches and installs that methodology, so it sits at a trust boundary: it pulls remote content over the network and writes it into a user's project. We welcome coordinated disclosure of any vulnerability in this repository. +PHARN is an audit-grade methodology — taking security seriously is part of the brand, not an afterthought. `pharn` is the bootstrapper that fetches and installs that methodology, so it sits at a trust boundary: it pulls remote content over the network and writes it into a user's project. We welcome coordinated disclosure of any vulnerability in this repository. -## What `pharn-cli` is, and its security surface +## What `pharn` is, and its security surface -This repository **is `pharn-cli`** — an ESM-only Node CLI (`"type": "module"`, NodeNext, `engines.node >= 20`) that runs a wizard, fetches the selected PHARN modules from `pharn-dev/pharn-oss` via `degit`, copies them into the user's `.claude/`, and writes `pharn.config.json`. It has a small, thin dependency set (`@clack/prompts`, `degit`, `minimist`, `picocolors`), no bundled runtime services, and no telemetry. Its security-relevant surface is exactly the two things that cross a trust boundary: **remote input** (the `manifest.json` / `module.json` it reads and the repo content it clones) and **file-system writes** (everything it copies into `.claude/` and the config it writes). +This repository **is `pharn`** — an ESM-only Node CLI (`"type": "module"`, NodeNext, `engines.node >= 20`) that runs a wizard, fetches the selected PHARN modules from `pharn-dev/pharn-oss` via `degit`, copies them into the user's `.claude/`, and writes `pharn.config.json`. It has a small, thin dependency set (`@clack/prompts`, `degit`, `minimist`, `picocolors`), no bundled runtime services, and no telemetry. Its security-relevant surface is exactly the two things that cross a trust boundary: **remote input** (the `manifest.json` / `module.json` it reads and the repo content it clones) and **file-system writes** (everything it copies into `.claude/` and the config it writes). The CLI's security model is **deterministic, not model-driven**: it never asks an AI to decide what is safe. Every value that arrives from the network is validated against strict regex allowlists, rejected for `..` and control characters, and every copy is confined with a `safeJoin` guard so nothing can escape its intended target — checks that hold regardless of what the fetched content says. Preserve that shape: a security fix that relies on "the content will be well-behaved" is not a fix. ## Supported versions -`pharn-cli` is published to npm as [`pharn-cli`](https://www.npmjs.com/package/pharn-cli) and is typically run via `npx pharn init`. We patch security issues against the **latest** published version only; `npx pharn@latest ...` always resolves to a supported release. +`pharn` is published to npm as [`pharn`](https://www.npmjs.com/package/pharn) and is typically run via `npx pharn init`. We patch security issues against the **latest** published version only; `npx pharn@latest ...` always resolves to a supported release. | Version | Supported | | -------- | ------------------ | | Latest | :white_check_mark: | | < Latest | :x: | -Because the CLI is normally invoked through `npx`, most users run the latest version automatically. If you have a pinned or globally installed copy, update it (`npm i -g pharn-cli@latest`) or invoke `npx pharn@latest` to pick up fixes. +Because the CLI is normally invoked through `npx`, most users run the latest version automatically. If you have a pinned or globally installed copy, update it (`npm i -g pharn@latest`) or invoke `npx pharn@latest` to pick up fixes. ## Reporting a vulnerability @@ -25,7 +25,7 @@ Because the CLI is normally invoked through `npx`, most users run the latest ver Instead, report privately through one of these channels: -1. **GitHub Security Advisories (preferred)** — use [private vulnerability reporting](https://github.com/pharn-dev/pharn-cli/security/advisories/new) to open a confidential report. No email is exposed and the report stays embargoed until a fix ships. +1. **GitHub Security Advisories (preferred)** — use [private vulnerability reporting](https://github.com/pharn-dev/pharn/security/advisories/new) to open a confidential report. No email is exposed and the report stays embargoed until a fix ships. 2. **Email** — if you cannot use GitHub advisories, email `support@pharn.dev` with `[PHARN SECURITY]` in the subject. Please include as much of the following as you can — it speeds up triage: @@ -33,7 +33,7 @@ Please include as much of the following as you can — it speeds up triage: - The type of issue (e.g. path traversal, command injection, SSRF, supply-chain, insufficient input validation). - Full paths of the source file(s) involved (step, lib, command, or config). - The location of the affected code (tag/branch/commit or a direct URL). -- The published `pharn-cli` version and Node.js version, if relevant. +- The published `pharn` version and Node.js version, if relevant. - Any configuration or stack choices required to reproduce. - Step-by-step reproduction instructions. - Proof-of-concept, if you have one. @@ -62,7 +62,7 @@ Scope follows the surface described above: everything that touches remote input ### Out of scope -- Vulnerabilities in the **PHARN skills, hooks, or runners** that `pharn-cli` installs — report those against the skills repository (e.g. its own `SECURITY.md`), not here. +- Vulnerabilities in the **PHARN skills, hooks, or runners** that `pharn` installs — report those against the skills repository (e.g. its own `SECURITY.md`), not here. - Vulnerabilities in **third-party AI tools** the installed stack targets (Claude Code, Codex, Cursor). - Issues in **vendor libraries** the docs reference (Stripe, Drizzle, Supabase, etc.) — report those upstream. - Vulnerabilities in the CLI's **own npm dependencies** that are fixed by an upstream patch — please report to that project (and feel free to flag it to us so we can bump the floor). @@ -71,7 +71,7 @@ Scope follows the surface described above: everything that touches remote input ## Security best practices for users -`pharn-cli`'s input validation and consent prompts are defense-in-depth, not a guarantee. When using the CLI: +`pharn`'s input validation and consent prompts are defense-in-depth, not a guarantee. When using the CLI: 1. **Run it in an existing, version-controlled project** so you can diff exactly what `init` wrote (`.claude/` and `pharn.config.json`) before committing. 2. **Review the vendor skills** the wizard offers before accepting them — vendor selection is opt-in and nothing is selected by default; only accept vendors you recognize. diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md index 9ae4fdf..aeef2df 100644 --- a/THREAT-MODEL.md +++ b/THREAT-MODEL.md @@ -2,10 +2,10 @@ file: "THREAT-MODEL.md" trust: trusted editable_by: "human only" -purpose: "The security foundation for pharn-cli. Defines the surfaces, the attack surface of consuming untrusted remote content, and how the validation floor answers each. Elaborates P2; never contradicts CONSTITUTION.md." +purpose: "The security foundation for pharn. Defines the surfaces, the attack surface of consuming untrusted remote content, and how the validation floor answers each. Elaborates P2; never contradicts CONSTITUTION.md." --- -# pharn-cli — Threat Model +# pharn — Threat Model > Read `CONSTITUTION.md` (esp. P0, P2) and `ARCHITECTURE.md §2, §5, §7` first. @@ -15,20 +15,20 @@ purpose: "The security foundation for pharn-cli. Defines the surfaces, the attac Conflating "the code we install" with "the code we run" is the most common mistake here. -- **Surface A — the content pharn-cli _writes_ into the user's repo.** pharn-cli copies PHARN - methodology files (markdown + a few `.cjs`/`.mjs`) into `.claude/`. **pharn-cli never executes +- **Surface A — the content pharn _writes_ into the user's repo.** pharn copies PHARN + methodology files (markdown + a few `.cjs`/`.mjs`) into `.claude/`. **pharn never executes them** — Claude Code does, later, on the user's machine. Whether that methodology is itself correct - or safe is pharn-oss's concern and the user's review, not pharn-cli's runtime. **Not the subject of + or safe is pharn-oss's concern and the user's review, not pharn's runtime. **Not the subject of this document.** -- **Surface B — pharn-cli _itself_ consuming hostile remote input.** A compromised, forked, or +- **Surface B — pharn _itself_ consuming hostile remote input.** A compromised, forked, or MITM'd source repo serving a poisoned `manifest.json` / `module.json` / `degit` tree. This is **architecture** — where the trust boundaries sit — and cannot be bolted on later. **This document is B.** -- **Surface B′ — the dev-loop _building_ pharn-cli**, an agent reading hostile context (an issue, a +- **Surface B′ — the dev-loop _building_ pharn**, an agent reading hostile context (an issue, a PR, another model's output). Answered by the `writes:`-scope + trusted-file write-guard hooks (`ARCHITECTURE.md §3.3`). -The framing axiom: **pharn-cli may not assume the remote is honest just because the URL says +The framing axiom: **pharn may not assume the remote is honest just because the URL says `pharn-dev/pharn-oss`.** Defense rests on structural validation independent of "the repo is ours" — the floor (`ARCHITECTURE.md §2`). @@ -36,7 +36,7 @@ the floor (`ARCHITECTURE.md §2`). ## 2. B's attack surface (name it explicitly) -pharn-cli fetches a manifest, per-module `module.json`, and `degit`-clones a subtree, then copies +pharn fetches a manifest, per-module `module.json`, and `degit`-clones a subtree, then copies files into the user's `.claude/`. The concrete surface: 1. **Malicious `installs` / skill path** — a `module.json` `installs` map or a skill `from` path @@ -72,7 +72,7 @@ Every answer reduces to the floor (P0) or is labeled a limit (`LIMITS.md`). ## 4. Residuals the design accepts (labeled, not hidden — `LIMITS.md`) -- **4a. Provenance, not verification.** pharn-cli trusts the configured source repo by **provenance** +- **4a. Provenance, not verification.** pharn trusts the configured source repo by **provenance** (plus validation), not by a signature over a release. A compromised upstream serving valid-**shaped** but malicious methodology passes the structural floor. _Backstop:_ the floor still contains **where** bytes land (`safeJoin`) and **how** they are fetched (guards) — a hostile upstream is bounded to @@ -85,13 +85,13 @@ Every answer reduces to the floor (P0) or is labeled a limit (`LIMITS.md`). ## 5. The one residual (named, bounded, not zeroed) -pharn-cli validates the **structure** of what it installs — paths contained, schema known, fetch +pharn validates the **structure** of what it installs — paths contained, schema known, fetch bounded — but it does not, and cannot, validate the **semantic safety** of the PHARN methodology content it copies verbatim into `.claude/`. **"It installed cleanly" means "it landed where it should without escaping," NOT "the installed methodology is correct or safe to run."** That judgment belongs to pharn-oss (the source) and the user's review. -Co-located: when the **dev-loop** reviews the pharn-cli code it builds, a finding's free-text +Co-located: when the **dev-loop** reviews the pharn code it builds, a finding's free-text (`problem`, `evidence`) inherits the reviewed code's untrusted tag (`ARCHITECTURE.md §8`) — bounded by the enum-gated split, not zeroed. This is the one place the trust model rests on **provenance + review**, not on the floor. diff --git a/docs/README.md b/docs/README.md index 6b4a151..f5710c9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # PHARN CLI documentation -`pharn-cli` installs [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code — into your project. Run `pharn init` to detect your project's archetype(s) and install the applicable PHARN **capabilities** (grillers + lenses) from `pharn-dev/pharn-oss` into the mirrored layout (`.claude/` + `pharn/`), copying the canonical constitution and writing `pharn.config.json`. +`pharn` installs [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code — into your project. Run `pharn init` to detect your project's archetype(s) and install the applicable PHARN **capabilities** (grillers + lenses) from `pharn-dev/pharn-oss` into the mirrored layout (`.claude/` + `pharn/`), copying the canonical constitution and writing `pharn.config.json`. ## Getting started diff --git a/docs/commands/init.md b/docs/commands/init.md index 88fa75e..73ac9f5 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -108,7 +108,7 @@ On success, the CLI reports the capability count and suggests opening Claude Cod ## Legacy configs -`init` always writes an **archetype** config. The `add`, `remove`, `list`, `update`, and `status` commands still understand an older **module**-based `pharn.config.json` (from a pre-archetype install) and fall back to the module/manifest path for it — so an existing legacy install keeps working. Only `init` is archetype-only. +`init` always writes an **archetype** config, and every command is archetype-only. A pre-archetype **module**-based `pharn.config.json` (one with `modules[]` but no `capabilities[]`, from a much older release) is no longer supported: `add`, `remove`, `list`, `update`, and `status` detect it up front and exit with a message to re-run `pharn init` — there is **no** module/manifest fallback (live pharn-oss ships no `manifest.json`). The config schema is additive, so a legacy config's now-unused fields (`modules`, `constitution`, `stackAnswers`, `installedSkills`) still parse; only the absence of `capabilities[]` triggers the rejection. ## Related diff --git a/docs/contributing.md b/docs/contributing.md index 4890a23..4873a5c 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,11 +1,11 @@ -# Contributing to pharn-cli +# Contributing to pharn -Development guide for the pharn-cli package. This is the full guide; the root [`CONTRIBUTING.md`](../CONTRIBUTING.md) is the quick-start pointer. +Development guide for the pharn package. This is the full guide; the root [`CONTRIBUTING.md`](../CONTRIBUTING.md) is the quick-start pointer. ## Setup ```bash -cd pharn-cli +cd pharn npm install ``` @@ -30,7 +30,7 @@ From the `test-app/` directory (which needs its own `package.json`) after `build npx pharn init ``` -Published package `pharn-cli` exposes both `pharn` and `pharn-cli` bins (see `package.json`). +Published package `pharn` exposes a single `pharn` bin (see `package.json`). ## Quality gates @@ -57,7 +57,7 @@ A separate CodeQL workflow analyzes the JavaScript/TypeScript surface on PRs, pu ## Project layout ```text -pharn-cli/ +pharn/ src/ index.ts CLI entry, command routing commands/ init, add, remove, update, list, status diff --git a/docs/getting-started.md b/docs/getting-started.md index 12419f4..c279705 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -22,12 +22,10 @@ You can continue after any warning by confirming. ## Running the CLI -The npm package name is **`pharn-cli`**. It installs two equivalent commands: **`pharn`** (preferred) and **`pharn-cli`**. +The npm package name is **`pharn`**. It installs a single command, **`pharn`**. ```bash npx pharn init -# same entry point -npx pharn-cli init ``` ## Quick start diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f08c67d..d2fc124 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -113,7 +113,7 @@ Run `pharn --help`. Valid commands: `init`, `add`, `remove`, `update`, `list`, ` | Problem | Fix | | ------- | --- | -| `pharn` not found after editing CLI | `npm run build:install-local` from `pharn-cli/` | +| `pharn` not found after editing CLI | `npm run build:install-local` from `pharn/` | | Type errors in tests | `npm run typecheck` | | Stale dist | `npm run build` | diff --git a/package.json b/package.json index ddbdd88..0f00302 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/pharn-dev/pharn-cli.git" + "url": "git+https://github.com/pharn-dev/pharn.git" }, "bugs": { - "url": "https://github.com/pharn-dev/pharn-cli/issues" + "url": "https://github.com/pharn-dev/pharn/issues" }, - "homepage": "https://github.com/pharn-dev/pharn-cli#readme", + "homepage": "https://github.com/pharn-dev/pharn#readme", "type": "module", "bin": { "pharn": "dist/index.js" @@ -31,7 +31,7 @@ }, "publishConfig": { "access": "public", - "provenance": true + "provenance": false }, "scripts": { "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc --noEmit && node scripts/build.mjs", diff --git a/src/types.ts b/src/types.ts index 5fea0fe..2ecffc9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -138,7 +138,7 @@ export interface PharnConfig { export type Layout = 'pharn' | 'flat'; // --------------------------------------------------------------------------- -// Capability resolver — archetype detection + capability selection. pharn-cli +// Capability resolver — archetype detection + capability selection. pharn // reads pharn-oss's capability index and selects which grillers/lenses apply to // a project (ARCHITECTURE.md §5). Pure + deterministic; the untrusted index is // parsed + validated at the fetch boundary (a later increment). @@ -158,7 +158,7 @@ export type Layout = 'pharn' | 'flat'; export type Archetype = 'ssr' | 'backend' | 'spa' | 'lib'; // One capability in the pharn-oss-published index. pharn-oss owns the -// authoritative schema; this is pharn-cli's consumer-side shape. `role` is +// authoritative schema; this is pharn's consumer-side shape. `role` is // narrowed to the installable kinds shipped today (grillers + lenses); the full // ARCHITECTURE.md §3.1 role enum is not needed here until an installable // skill/auditor ships. `applies` is 'universal' (always selected) or the From 9b889fc69ea88950f0d512803e209f88e99b1653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 16:33:06 +0200 Subject: [PATCH 02/10] chore: rename npm package to @pharn-dev/pharn and sync stale docs/comments Rename the npm package `pharn` -> the scoped `@pharn-dev/pharn` across all user-facing surfaces: package.json `name`, README (badge + npx install), docs/, CONTRIBUTING, SECURITY, CHANGELOG, the GitHub issue templates, the `pharn -h` USAGE text (src/index.ts), and the git-prereq error message (src/steps/prereqs.ts). The installed bin stays `pharn` (unscoped); only the npm package identity is scoped. Also correct stale in-code comments left by the module-model removal: src/commands/init.ts (no manifest fallback), src/lib/constitution.ts (test-only now that wizard.ts is gone), and src/lib/repo.ts (capability index, not manifest.json). Fix a CHANGELOG garble the earlier repo-wide pharn-cli->pharn rename introduced (`pharn` -> `pharn`), restoring the historical `pharn-cli`. Co-Authored-By: Claude Opus 4.8 --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++-- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- CHANGELOG.md | 12 ++++++------ CLAUDE.md | 17 ++++++++++------- CONTRIBUTING.md | 2 +- README.md | 14 +++++++------- SECURITY.md | 8 ++++---- docs/contributing.md | 8 ++++---- docs/getting-started.md | 6 +++--- docs/troubleshooting.md | 6 +++--- package.json | 8 ++++---- src/commands/init.ts | 4 ++-- src/index.ts | 2 +- src/lib/constitution.ts | 7 +++++-- src/lib/repo.ts | 6 +++--- src/steps/prereqs.ts | 2 +- 16 files changed, 58 insertions(+), 52 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ead9e8e..3ad8245 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ A clear description of the bug. - **Command**: - **Step / area**: -- **`pharn` version** (`npx pharn --version`, or `pharnVersion` in `pharn.config.json`): +- **`pharn` version** (`npx @pharn-dev/pharn --version`, or `pharnVersion` in `pharn.config.json`): - **Skills version** (`.claude/SKILLS_VERSION`, or `skillsVersion` in `pharn.config.json`): ## Steps to reproduce @@ -36,7 +36,7 @@ What actually happened. Paste the relevant CLI output (run with `PHARN_DEBUG=1` - Node.js version (`node --version`): - npm version (`npm --version`): - OS: -- Install method: +- Install method: - Target project: ## Additional context diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 827b9d4..3d192ee 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: true contact_links: - name: Security vulnerability - url: https://github.com/pharn-dev/pharn/security/advisories/new + url: https://github.com/pharn-dev/pharn-cli/security/advisories/new about: Please report security issues privately — do not open a public issue. See SECURITY.md. - name: Documentation - url: https://github.com/pharn-dev/pharn/tree/main/docs + url: https://github.com/pharn-dev/pharn-cli/tree/main/docs about: Getting started, stack options, and command reference. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9767a..c1fa8d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,11 +32,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Renamed the npm package `pharn` → `pharn`** and made it publish-ready — added `repository`, +- **Renamed the npm package `pharn-cli` → `@pharn-dev/pharn`** and made it publish-ready — added `repository`, `bugs`, `homepage`, `keywords`, and `publishConfig` (public access + provenance); dropped the - `pharn` bin alias for a single `pharn` bin; and added a `prepack` build so `npm publish` always + `pharn-cli` bin alias for a single `pharn` bin; and added a `prepack` build so `npm publish` always ships a freshly compiled `dist/`. No CLI behavior change and `version` is unchanged; the package now - installs via `npx pharn@latest init`. + installs via `npx @pharn-dev/pharn@latest init`. - Docs: surfaced the new optional `/pharn-spec` stage (intent capture before `/pharn-plan`) in getting-started and the `pharn-pipeline` module description, matching `pharn-oss`. No CLI code change — `/pharn-spec` ships transparently via the existing whole-module install from `main`. @@ -127,6 +127,6 @@ Next.js project. Exposes both `pharn` and `pharn` bins. `pharn.config.json`. It does not yet install npm packages or scaffold the stack — that is planned for v0.2 (see `docs/roadmap.md` and the `TODO(v0.2)` markers). -[Unreleased]: https://github.com/pharn-dev/pharn/compare/v0.2.0...HEAD -[0.2.0]: https://github.com/pharn-dev/pharn/compare/v0.1.0...v0.2.0 -[0.1.0]: https://github.com/pharn-dev/pharn/releases/tag/v0.1.0 +[Unreleased]: https://github.com/pharn-dev/pharn-cli/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/pharn-dev/pharn-cli/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/pharn-dev/pharn-cli/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md index 1bdccfe..efb5fdf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,18 +11,21 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands ```bash -npm run dev -- init # run the CLI via tsx (pass args after --) -npm run build # tsc --noEmit (typecheck) → esbuild bundle+minify → dist/index.js -npm run build:install-local # build + symlink pharn into local test-app/node_modules +npx tsx src/index.ts init # run the CLI from source via tsx (there is no `dev` script) +npm run build # rm dist → tsc --noEmit (typecheck) → esbuild bundle+minify → dist/index.js +npm run build:install-local # build + install dist/bin into every local test-*/ app's node_modules npm run typecheck # tsc --noEmit for src AND tests (two configs) npm run lint # eslint src +npm run lint:md # markdownlint-cli2 over docs/**/*.md + *.md npm run format:check # prettier check (use `format` to write) npm test # vitest run (single pass) npm run test:watch +npm run test:coverage # vitest run --coverage (CI runs this) +npm run check # format:check + lint + typecheck + test (aggregate; excludes lint:md) npx vitest run tests/install-capabilities.test.ts # single test file ``` -CI (`.github/workflows/ci.yml`) runs format:check, lint, typecheck, and test — all four must pass. `PHARN_DEBUG=1` enables full error output for fetch/install failures. +CI (`.github/workflows/ci.yml`) runs six gates independently (so one failure can't mask the others) — format:check, lint, lint:md, typecheck, test (via `test:coverage`), and build — all must pass. `PHARN_DEBUG=1` enables full error output for fetch/install failures. ## ESM / module conventions @@ -38,19 +41,19 @@ ESM-only (`"type": "module"`, NodeNext). **Relative imports must use `.js` exten 2. `detectArchetypesFromProject` (`lib/detect-archetype.ts`) — merges `package.json` dependency names + a bounded, symlink-safe file-tree walk into an `Archetype[]` (`ssr`/`backend`/`spa`/`lib`). 3. `fetchRepo` (`lib/repo.ts`) — degit-clone pharn-oss to a temp dir (cleaned up in a `finally`; every `process.exit`/`cancelAndExit` happens AFTER it). 4. `parseCapabilityIndex` (`lib/capability-index.ts`) + `resolveCapabilities` (`lib/resolve-capabilities.ts`) — select capabilities whose `applies` is `universal` or intersects the detected archetypes; skip the rest with a reason. -5. `runArchetypeSummary` (selected + skipped) → `install` / `cancel`; on `install`, `confirmOverwriteIfExists` guards an existing config, then `runInstallArchetype` (`steps/install-archetype.ts` → `lib/install-capabilities.ts`) copies the capabilities + fixed product surfaces into the mirrored layout (flat OR `pharn/`) and writes the archetype `pharn.config.json` (`archetypes`, `capabilities`, `layout`, `skillsVersion` from `SKILLS_VERSION`, `modules: []`; canonical `CONSTITUTION.md` copied verbatim). The `--archetype` CLI flag is a retained no-op alias for one release. +5. `runArchetypeSummary` (selected + skipped) → `install` / `cancel`; on `install`, `confirmOverwriteIfExists` guards an existing config, then `runInstallArchetype` (`steps/install-archetype.ts` → `lib/install-capabilities.ts`) copies the capabilities + fixed product surfaces into the mirrored layout (flat OR `pharn/`) and writes the archetype `pharn.config.json` (`archetypes`, `capabilities`, `layout`, `skillsVersion` read from the fetched `SKILLS_VERSION` file via `readSkillsVersion`, `modules: []`, plus `models`/`seam` defaults; canonical `CONSTITUTION.md` copied verbatim). The `--archetype` CLI flag is a retained no-op alias for one release. **`lib/install-capabilities.ts`** is the shared capability copy core used by init/add/update. `installCapabilityDirs(repoDir, projectRoot, capabilities, paths?)` pre-flights **every** selected capability source (validated name via `CAPABILITY_NAME_RE` + `safeJoin` + existence + symlink rejection) before any write — no partial installs — then copies each griller/lens dir into the mirrored layout (`add` uses this alone). `installCapabilities` additionally copies the fixed product surfaces: product `pharn-*` commands (excluding `pharn-dev-*`), `.cjs` hooks (excluding `*.test.cjs`), `settings.json` (**never** overwritten), the trusted docs, `pharn-contracts/`, and `.dev/floor/` minus test files. Copying from the untrusted clone is symlink-guarded (`isSymlink` reject / `noSymlinks` filter) and `safeJoin`-contained; file contents are copied verbatim, never executed. The install set is resolved by `lib/capability-index.ts` (`parseCapabilityIndex` — the untrusted-frontmatter → typed `CapabilityIndex` fetch boundary, reading only `name`/`role`/`applies` via a strict field reader) + `lib/resolve-capabilities.ts` (select where `applies` is `universal` or intersects the detected archetypes). The commit SHA is threaded from `fetchRepo` (`repo.sha`) — no separate GitHub fetch (closes the resolve/fetch TOCTOU). **`lib/validate.ts` is security-sensitive.** Untrusted names, versions, paths, and capability frontmatter are validated against strict regex/enum allowlists (`CAPABILITY_NAME_RE`, `VERSION_RE`, `INSTALL_PATH_RE`, `COPY_FILENAME_RE`, the `role`/`applies` enums), checked for `..`, and rejected on control chars. **`safeJoin` lives here** (relocated from the deleted `install-modules.ts`) — the lexical path-containment gate that `install-capabilities.ts`, `diff.ts`, `capability-index.ts`, `layout.ts`, `skills-version.ts`, and `remove.ts` all guard their fs access with, so nothing escapes its base dir (`install-capabilities.ts` adds a symlink-aware backstop at the write sites). Remote fetches (`skills-version.ts`) use `redirect: 'error'`, an 8s timeout, and a 256KB body cap. Preserve these invariants. -**`lib/pharn-config.ts`** reads/writes `pharn.config.json` (`pharnVersion`, `skillsVersion`, `repo`, `commit`, `installedAt`, and — for an archetype install — `archetypes[]`, `capabilities[]` (`{name, role}`), `layout`, plus the `models`/`seam` blocks). Schema is additive — a legacy config's now-unused `modules[]`/`constitution`/`stackAnswers`/`installedSkills[]` still load (P7). `isArchetypeConfig` (= `Array.isArray(config.capabilities)`) is the deterministic discriminator; **`loadArchetypeConfigOrExit`** is the shared load-or-reject surface for `add`/`update`/`status`/`remove` (a pre-archetype config → `LEGACY_CONFIG_MESSAGE` + exit(1), never a fetch; `list` keeps its own json-aware check so `--json` stderr stays clean). `add`/`update`/`remove` update the config in place; none touches `CONSTITUTION.md`. +**`lib/pharn-config.ts`** reads/writes `pharn.config.json` (`pharnVersion`, `skillsVersion`, `repo`, `commit`, `installedAt`, and — for an archetype install — `archetypes[]`, `capabilities[]` (`{name, role}`), `layout`, plus the `models`/`seam` blocks). A present-but-invalid `models`/`seam` block is caught by its own validator — `lib/model-routing.ts` (`validateModelRouting`/`ModelRoutingError`) and `lib/seam-config.ts` (`validateSeamConfig`/`SeamConfigError`) — and `readPharnConfig` lets that named error PROPAGATE (never collapsing a bad hand-edit into the "run init" path); `isConfigValidationError` + `loadConfigOrExit` catch and report it, and the validated/stripped blocks replace the raw ones. Schema is additive — a legacy config's now-unused `modules[]`/`constitution`/`stackAnswers`/`installedSkills[]` still load (P7). `isArchetypeConfig` (= `Array.isArray(config.capabilities)`) is the deterministic discriminator; **`loadArchetypeConfigOrExit`** is the shared load-or-reject surface for `add`/`update`/`status`/`remove` (a pre-archetype config → `LEGACY_CONFIG_MESSAGE` + exit(1), never a fetch; `list` keeps its own json-aware check so `--json` stderr stays clean). `add`/`update`/`remove` update the config in place; none touches `CONSTITUTION.md`. **`pharn add` addressing** (`commands/add.ts` + `lib/capability-address.ts`). `add ` or `add :` (e.g. `add a11y`, `add lens:n-plus-one`) installs one capability into an archetype project — a manual override of archetype auto-selection. It clones pharn-oss (SHA-pinned), resolves the arg against `parseCapabilityIndex`, and if it uniquely names a not-yet-installed capability, copies it via `installCapabilityDirs` and **appends** to `capabilities` (never touches `archetypes`). Already-installed → no-op; unknown/ambiguous → lists the valid `role:name` addresses. `pharn update` re-resolves the **recorded archetypes** against the latest index and re-copies (mirrors the legacy update's re-resolve-recorded step). **`pharn remove` addressing** (`commands/remove.ts`) is the inverse of `add`. `remove ` / `remove :` (no arg → an interactive picker over the installed capabilities) deletes that one isolated capability dir — addressed at the project's recorded `layout` (flat `pharn-review` / `pharn-pipeline/grillers/`, OR the same under `pharn/`, via `configLayout` + `layoutPaths`) — and drops its `capabilities` entry. **No clone, no network** — everything is derivable from `config.capabilities` + the filesystem, so `remove.ts` imports no repo module at all; `archetypes` is never touched, and `CONSTITUTION.md`/`memory-bank/` are never in a capability dir. Not-installed → benign no-op listing the removable capabilities; a name installed in both roles → hard-fail (ambiguous). `--yes`/`-y` is a no-op (there is no confirm prompt to skip). Every delete path is `safeJoin`-contained. -**`pharn status` (`commands/status.ts`) is strictly read-only** — it never writes, deletes, or overwrites (fixing is `update`/`add`). Two sections: a **version** check (installed `skillsVersion` vs upstream `SKILLS_VERSION`, plus an archetype + capability-count summary) and a **drift** check. Default clones `@main` once and reuses it for both; `--no-drift` skips the clone and uses `fetchRemoteSkillsVersion` for the version section only; `--strict` exits 1 on any outdated/modified/missing (CI gate, default exit 0). Cleanup runs in a `finally`, and every `process.exit` happens *after* it. The pure (no I/O) engine is **`lib/diff.ts` → `diffInstalledCapabilities`**: it mirrors `installCapabilities` to derive the **expected** file set — the selected capability dirs + the fixed product surfaces, at the recorded `layout` — then `sha256`-compares each against the project root, returning `{modified, missing, okCount}`. Every read is `safeJoin`-guarded (from `lib/validate.ts`). `.claude/settings.json` is user-owned (preserved at install) and excluded; the copied-verbatim trusted docs, hooks, contracts, and floor checkers ARE compared. Drift is derived live from the clone (no stored hashes), always against `@main`, never the pinned `commit`. +**`pharn status` (`commands/status.ts`) is strictly read-only** — it never writes, deletes, or overwrites (fixing is `update`/`add`). Two sections: a **version** check (installed `skillsVersion` vs the upstream `SKILLS_VERSION` file, plus an archetype + capability-count summary) and a **drift** check. Default clones `@main` once and reuses it for both; `--no-drift` skips the clone and uses `fetchRemoteSkillsVersion` for the version section only; `--strict` exits 1 on any outdated/modified/missing (CI gate, default exit 0). Cleanup runs in a `finally`, and every `process.exit` happens *after* it. The pure (no I/O) engine is **`lib/diff.ts` → `diffInstalledCapabilities`**: it mirrors `installCapabilities` to derive the **expected** file set — the selected capability dirs + the fixed product surfaces, at the recorded `layout` — then `sha256`-compares each against the project root, returning `{modified, missing, okCount}`. Every read is `safeJoin`-guarded (from `lib/validate.ts`). `.claude/settings.json` is user-owned (preserved at install) and excluded; the copied-verbatim trusted docs, hooks, contracts, and floor checkers ARE compared. Drift is derived live from the clone (no stored hashes), always against `@main`, never the pinned `commit`. ## Testing diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f20f53..1940c4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Before changing anything, read these in order: ## The 30-second version 1. **Open an issue first** for any non-trivial change. PHARN is small-surface on purpose. -2. **Install**: `cd pharn && npm install` (dev-only tooling; only `dist/` ships to npm). +2. **Install**: `cd pharn-cli && npm install` (dev-only tooling; only `dist/` ships to npm). 3. **Run the gates before pushing** — all four must pass (this is exactly what CI runs): `npm run format:check` · `npm run lint` · `npm run typecheck` · `npm test` 4. **Branch**: `feat/…`, `fix/…`, or `docs/…`. diff --git a/README.md b/README.md index e35670a..10c7d62 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ The installer for [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code that keeps comprehension debt legible instead of silent. -[![npm](https://img.shields.io/npm/v/pharn)](https://www.npmjs.com/package/pharn) -[![CI](https://github.com/pharn-dev/pharn/actions/workflows/ci.yml/badge.svg)](https://github.com/pharn-dev/pharn/actions/workflows/ci.yml) -[![CodeQL](https://github.com/pharn-dev/pharn/actions/workflows/codeql.yml/badge.svg)](https://github.com/pharn-dev/pharn/actions/workflows/codeql.yml) +[![npm](https://img.shields.io/npm/v/@pharn-dev/pharn)](https://www.npmjs.com/package/@pharn-dev/pharn) +[![CI](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml) +[![CodeQL](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green)](./LICENSE) [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](./package.json) ```bash -npx pharn init +npx @pharn-dev/pharn init ```
@@ -38,7 +38,7 @@ npx pharn init ## Install ```bash -npx pharn@latest init +npx @pharn-dev/pharn@latest init ``` `pharn` runs straight from npm — `npx` fetches the latest published version and runs it in your project; no global install. Requires a git-initialized project and Node >= 20. @@ -51,7 +51,7 @@ Vibe-coding with an AI agent is fast — until the chat history scrolls away and `pharn` is how you get it. Run `pharn init` in your project; the CLI detects your project's **archetype(s)**, fetches the applicable PHARN **capabilities** from `pharn-dev/pharn-oss`, copies them plus the canonical constitution into the mirrored layout (`.claude/` + `pharn/`), and writes `pharn.config.json`. -> The npm package is `pharn`; it installs a single `pharn` binary. +> The npm package is `@pharn-dev/pharn`; it installs a single `pharn` binary. --- @@ -127,7 +127,7 @@ Full reference: **[docs/](docs/README.md)** ## Development ```bash -cd pharn +cd pharn-cli npm install npm run dev # e.g. npm run dev -- init npm run build diff --git a/SECURITY.md b/SECURITY.md index 8abe9fa..e4397a1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,14 +10,14 @@ The CLI's security model is **deterministic, not model-driven**: it never asks a ## Supported versions -`pharn` is published to npm as [`pharn`](https://www.npmjs.com/package/pharn) and is typically run via `npx pharn init`. We patch security issues against the **latest** published version only; `npx pharn@latest ...` always resolves to a supported release. +`pharn` is published to npm as [`@pharn-dev/pharn`](https://www.npmjs.com/package/@pharn-dev/pharn) and is typically run via `npx @pharn-dev/pharn init`. We patch security issues against the **latest** published version only; `npx @pharn-dev/pharn@latest ...` always resolves to a supported release. | Version | Supported | | -------- | ------------------ | | Latest | :white_check_mark: | | < Latest | :x: | -Because the CLI is normally invoked through `npx`, most users run the latest version automatically. If you have a pinned or globally installed copy, update it (`npm i -g pharn@latest`) or invoke `npx pharn@latest` to pick up fixes. +Because the CLI is normally invoked through `npx`, most users run the latest version automatically. If you have a pinned or globally installed copy, update it (`npm i -g @pharn-dev/pharn@latest`) or invoke `npx @pharn-dev/pharn@latest` to pick up fixes. ## Reporting a vulnerability @@ -25,7 +25,7 @@ Because the CLI is normally invoked through `npx`, most users run the latest ver Instead, report privately through one of these channels: -1. **GitHub Security Advisories (preferred)** — use [private vulnerability reporting](https://github.com/pharn-dev/pharn/security/advisories/new) to open a confidential report. No email is exposed and the report stays embargoed until a fix ships. +1. **GitHub Security Advisories (preferred)** — use [private vulnerability reporting](https://github.com/pharn-dev/pharn-cli/security/advisories/new) to open a confidential report. No email is exposed and the report stays embargoed until a fix ships. 2. **Email** — if you cannot use GitHub advisories, email `support@pharn.dev` with `[PHARN SECURITY]` in the subject. Please include as much of the following as you can — it speeds up triage: @@ -75,7 +75,7 @@ Scope follows the surface described above: everything that touches remote input 1. **Run it in an existing, version-controlled project** so you can diff exactly what `init` wrote (`.claude/` and `pharn.config.json`) before committing. 2. **Review the vendor skills** the wizard offers before accepting them — vendor selection is opt-in and nothing is selected by default; only accept vendors you recognize. -3. **Prefer `npx pharn@latest`** so you run the current, supported release rather than a stale pinned copy. +3. **Prefer `npx @pharn-dev/pharn@latest`** so you run the current, supported release rather than a stale pinned copy. 4. **Inspect the cloned `.claude/` skills** before running them through your AI tool — installation fetches remote content. 5. **Set `PHARN_DEBUG=1`** if a manifest or clone step fails unexpectedly, and report anything that looks like the CLI fetching or writing somewhere it should not. diff --git a/docs/contributing.md b/docs/contributing.md index 4873a5c..b41c001 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -5,7 +5,7 @@ Development guide for the pharn package. This is the full guide; the root [`CONT ## Setup ```bash -cd pharn +cd pharn-cli npm install ``` @@ -27,10 +27,10 @@ npm install From the `test-app/` directory (which needs its own `package.json`) after `build:install-local`: ```bash -npx pharn init +npx @pharn-dev/pharn init ``` -Published package `pharn` exposes a single `pharn` bin (see `package.json`). +Published package `@pharn-dev/pharn` exposes a single `pharn` bin (see `package.json`). ## Quality gates @@ -57,7 +57,7 @@ A separate CodeQL workflow analyzes the JavaScript/TypeScript surface on PRs, pu ## Project layout ```text -pharn/ +pharn-cli/ src/ index.ts CLI entry, command routing commands/ init, add, remove, update, list, status diff --git a/docs/getting-started.md b/docs/getting-started.md index c279705..c27aeba 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -22,10 +22,10 @@ You can continue after any warning by confirming. ## Running the CLI -The npm package name is **`pharn`**. It installs a single command, **`pharn`**. +The npm package name is **`@pharn-dev/pharn`**. It installs a single command, **`pharn`**. ```bash -npx pharn init +npx @pharn-dev/pharn init ``` ## Quick start @@ -35,7 +35,7 @@ npx create-next-app@latest my-app cd my-app npx shadcn@latest init git init && git add -A && git commit -m "init" -npx pharn init +npx @pharn-dev/pharn init ``` `pharn` with no subcommand runs `init` (same as `pharn init`). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d2fc124..80e18e5 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -24,7 +24,7 @@ The CLI message says "git not found" but the check is for a **`.git` directory** ```text ✗ git not found. Run: git init && git add -A && git commit -m 'init' - Then re-run: npx pharn init + Then re-run: npx @pharn-dev/pharn init ``` Exits with code **1**. @@ -65,7 +65,7 @@ Symptoms: Causes include a degit clone failure (network/GitHub), or a selected capability missing at its expected path (`//.md`) in the fetched repo. Set `PHARN_DEBUG=1` and re-run for the full stack trace: ```bash -PHARN_DEBUG=1 npx pharn init +PHARN_DEBUG=1 npx @pharn-dev/pharn init ``` ## Overwrite declined @@ -113,7 +113,7 @@ Run `pharn --help`. Valid commands: `init`, `add`, `remove`, `update`, `list`, ` | Problem | Fix | | ------- | --- | -| `pharn` not found after editing CLI | `npm run build:install-local` from `pharn/` | +| `pharn` not found after editing CLI | `npm run build:install-local` from `pharn-cli/` | | Type errors in tests | `npm run typecheck` | | Stale dist | `npm run build` | diff --git a/package.json b/package.json index 0f00302..1b2f0ff 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "pharn", + "name": "@pharn-dev/pharn", "version": "0.2.0", "description": "Audit-grade AI development methodology for Claude Code — spec, plan, grill, build, verify, ship.", "keywords": [ @@ -13,12 +13,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/pharn-dev/pharn.git" + "url": "git+https://github.com/pharn-dev/pharn-cli.git" }, "bugs": { - "url": "https://github.com/pharn-dev/pharn/issues" + "url": "https://github.com/pharn-dev/pharn-cli/issues" }, - "homepage": "https://github.com/pharn-dev/pharn#readme", + "homepage": "https://github.com/pharn-dev/pharn-cli#readme", "type": "module", "bin": { "pharn": "dist/index.js" diff --git a/src/commands/init.ts b/src/commands/init.ts index fbc1c53..4649538 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -27,8 +27,8 @@ export async function runInit(): Promise { // Archetype-driven install is the default (and only) init flow: detect the // project's archetype(s) and install the applicable capabilities. Framework- // agnostic — no module catalog / manifest fetch. (The legacy module/wizard - // flow was removed; add/update still fall back to the manifest for - // pre-archetype configs.) + // flow was removed entirely; add/update/status/remove reject a pre-archetype + // config up front via loadArchetypeConfigOrExit — there is no manifest fallback.) await runInitArchetype(); } diff --git a/src/index.ts b/src/index.ts index dfde9d9..e9e930c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { runList } from './commands/list.js'; import { runStatus } from './commands/status.js'; import { PHARN_VERSION } from './version.js'; -const USAGE = `Pharn - Installs PHARN (an audit-grade methodology for Claude Code) into your project. npx pharn init detects your project's archetype and installs the applicable PHARN capabilities; pharn add installs another capability later; pharn update bumps to the latest skills version. +const USAGE = `Pharn - Installs PHARN (an audit-grade methodology for Claude Code) into your project. npx @pharn-dev/pharn init detects your project's archetype and installs the applicable PHARN capabilities; pharn add installs another capability later; pharn update bumps to the latest skills version. Usage: pharn [command] [options] diff --git a/src/lib/constitution.ts b/src/lib/constitution.ts index f91510f..3e7528b 100644 --- a/src/lib/constitution.ts +++ b/src/lib/constitution.ts @@ -1,5 +1,8 @@ -// Pure (no-I/O) constitution transforms. Sits with lib/wizard.ts and lib/diff.ts -// as a testable helper used by the installer when materializing CONSTITUTION.md. +// Pure (no-I/O) constitution transforms. A testable helper retained from the +// legacy constitution-variant materialization; the archetype install copies +// pharn-oss's canonical CONSTITUTION.md verbatim, so no production command calls +// this today — it is exercised only by tests/constitution.test.ts (its former +// caller lib/wizard.ts was deleted). // Principle 2 is "Multi-Tenant Isolation" across every shipped constitution // variant. It is gated on the project's multi-tenant SaaS flag: a non-SaaS diff --git a/src/lib/repo.ts b/src/lib/repo.ts index d88ac89..fa9cc3c 100644 --- a/src/lib/repo.ts +++ b/src/lib/repo.ts @@ -23,9 +23,9 @@ export interface FetchedRepo { * Clone the whole pharn-oss repo into a fresh temp directory, PINNED to the * resolved commit SHA so the recorded provenance matches the fetched bytes * (closing the separate-resolve/separate-fetch TOCTOU where a push between the - * two reads made `commit` disagree with the tree). The caller reads - * manifest.json + each module's installs from it, records `sha` as `commit`, - * then calls cleanup(). + * two reads made `commit` disagree with the tree). The caller derives the + * capability index from the clone (lib/capability-index.ts), records `sha` as + * `commit`, then calls cleanup(). * * When the SHA cannot be resolved, fall back to REPO_BRANCH (LIMITS.md §3b — the * install still proceeds; `sha` is null). Provenance is by-SHA, NOT diff --git a/src/steps/prereqs.ts b/src/steps/prereqs.ts index c9a7515..221c024 100644 --- a/src/steps/prereqs.ts +++ b/src/steps/prereqs.ts @@ -8,7 +8,7 @@ export function runGitPrereq(): void { if (!existsSync(resolve(cwd, '.git'))) { fail( - "✗ git not found.\n Run: git init && git add -A && git commit -m 'init'\n Then re-run: npx pharn init", + "✗ git not found.\n Run: git init && git add -A && git commit -m 'init'\n Then re-run: npx @pharn-dev/pharn init", ); } } From 815acf2d1814dac20bd3737fb0889bd029ec686c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 16:33:06 +0200 Subject: [PATCH 03/10] ci: add npm publish workflow with provenance Publish to npm on a GitHub Release (or manual dispatch). Uses OIDC (id-token: write) + `npm publish --provenance` for a signed provenance attestation; the flag overrides package.json's `provenance: false`, so CI gets provenance while local `npm publish` stays functional. Requires an NPM_TOKEN repository secret (npm automation/granular token). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/publish.yml | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2b74c6d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,36 @@ +name: publish + +# Publish to npm when a GitHub Release is published, or on manual dispatch. +# Cut a release (tag vX.Y.Z, matching package.json "version") and this runs. +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + id-token: write # required for npm provenance (Sigstore attestation via OIDC) + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6 + with: + node-version: 20 + cache: npm + registry-url: https://registry.npmjs.org + - name: Install + run: npm ci + # `npm publish` runs prepublishOnly (npm run check) + prepack (build) first, + # so a failing gate blocks the release. `--provenance` overrides the + # publishConfig `provenance: false` default: CI gets a signed attestation + # (needs id-token: write + a CI OIDC provider) while local `npm publish` + # still works without one. + - name: Publish + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From afe7334def1e63995075dda625ee56071d929a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 19:43:59 +0200 Subject: [PATCH 04/10] ipdates --- .dev/features/canonical-npm-name/GRILL.md | 57 +++++++++ .dev/features/canonical-npm-name/PLAN.md | 119 ++++++++++++++++++ .../features/canonical-npm-name/REGRESSION.md | 54 ++++++++ .dev/features/canonical-npm-name/REVIEW.md | 75 +++++++++++ .dev/features/canonical-npm-name/SHIP.md | 48 +++++++ .dev/features/canonical-npm-name/VERIFY.md | 45 +++++++ .../canonical-npm-name/regression-report.json | 31 +++++ .../canonical-npm-name/verify-report.json | 18 +++ .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .pharn/pharn-dev-regress/base-results.json | 2 +- .pharn/pharn-dev-regress/head-results.json | 2 +- .pharn/pharn-dev-verify/results.json | 2 +- .pharn/writes-scope.json | 9 +- CHANGELOG.md | 5 +- README.md | 8 +- SECURITY.md | 6 +- docs/contributing.md | 4 +- docs/getting-started.md | 6 +- docs/troubleshooting.md | 4 +- package.json | 2 +- src/index.ts | 2 +- src/steps/prereqs.ts | 2 +- 22 files changed, 475 insertions(+), 30 deletions(-) create mode 100644 .dev/features/canonical-npm-name/GRILL.md create mode 100644 .dev/features/canonical-npm-name/PLAN.md create mode 100644 .dev/features/canonical-npm-name/REGRESSION.md create mode 100644 .dev/features/canonical-npm-name/REVIEW.md create mode 100644 .dev/features/canonical-npm-name/SHIP.md create mode 100644 .dev/features/canonical-npm-name/VERIFY.md create mode 100644 .dev/features/canonical-npm-name/regression-report.json create mode 100644 .dev/features/canonical-npm-name/verify-report.json diff --git a/.dev/features/canonical-npm-name/GRILL.md b/.dev/features/canonical-npm-name/GRILL.md new file mode 100644 index 0000000..775bcd9 --- /dev/null +++ b/.dev/features/canonical-npm-name/GRILL.md @@ -0,0 +1,57 @@ +# GRILL — canonical-npm-name (advisory) + +Plan under interrogation: `.dev/features/canonical-npm-name/PLAN.md`. +Spec-hash check: `sha256(ARCHITECTURE.md)` = `bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e` — **matches** the plan's `spec_content_hash`. No spec drift (the binding block on drift is `/pharn-dev-build`'s floor-gate; here it only confirms). + +> The PLAN is `trust: untrusted` to this griller. `problem`/`evidence` free-text below quotes the plan as DATA; the enum-gated fields (`type`/`rule_id`/`severity`/`file`) are this griller's own membership/path assertions. Grillers gate nothing — this whole log is advisory (fix #3). + +## Findings + +### Axis: testability (P1) + +```yaml +- type: FINDING + rule_id: "P1" + severity: minor # advisory assignment — a griller never gates (fix #3) + file: ".dev/features/canonical-npm-name/PLAN.md:65" + problem: "The rename of the user-facing help/error copy in src/index.ts (USAGE) and src/steps/prereqs.ts is not pinned by any test, so a future edit could silently reintroduce the deprecated scoped name without a red gate." + evidence: "None required... the existing tests/index.test.ts --help assertion checks toContain('Usage:'), not the package-name string, so it stays green whether or not the USAGE string changes." +``` + +Note: this is a copy-only string change (no control-flow), so the miss is low-risk and P1 is arguably satisfied vacuously — but a one-line assertion (USAGE contains `pharn`, not `@pharn-dev/pharn`) would make the rename a *tested* fact for ~free. Surfaced for the human; not a gate. + +### Axis: guarantee-audit / P0 + +```yaml +- type: FINDING + rule_id: "P0" + severity: minor # advisory assignment + file: ".dev/features/canonical-npm-name/PLAN.md:69" + problem: "The 'same file list as the scoped publish' guarantee is established by construction plus the local npm pack output, not by an actual list/byte diff against the already-published @pharn-dev/pharn@0.2.0 tarball (which is not fetched this run)." + evidence: "Published tarball is pharn-0.2.0.tgz, same file list as the scoped publish; content diffs = package.json.name + the bundled help/error strings in dist/index.js ... floor: npm pack --dry-run output." +``` + +Note: the floor that *does* hold deterministically is `npm pack --dry-run` → tarball name `pharn-0.2.0.tgz` + its file list (`package.json` + `dist/`). "Identical to the *scoped* publish" is a reasoning claim (only the name field + two bundled strings changed), honest to label as such rather than as a verified diff. Low-risk; the file SET is unchanged by construction (`files: ["dist"]`). + +### Axis: documentation (P7) — no finding + +The documentation griller's own rule: a **mechanical rename** may legitimately need no new docs, and this increment *is* the documentation update — README, SECURITY, `docs/**`, and CHANGELOG all move to the canonical name in-scope. The public surface (package name + install command) is documented, not shipped as self-explanatory. Declaration recognized → **no absence finding**. + +### Axes with no findings + +- **P2 (trust):** N/A — the increment ingests no untrusted artifact (no manifest/degit content); no taint introduced. Correctly stated in the plan. +- **P3 (one axis / no sibling imports):** every touched file changes for the single axis "canonical package name"; no command→command or step→step import is added. +- **P5 (determinism):** no new branch; the three open questions were resolved by asking the human at GATE 1 (terminal fallback = ask, honored). +- **P7 (scope):** real trigger (publish under the free canonical name); not speculative; one axis, not two bundled. + +## Tooling observation (not a plan finding) + +`node .dev/floor/count-grillers.mjs .` reported **81** registered grillers — but **all 81 are under `test-*/` install fixtures** (`test-backend/pharn/...`, `test-full/pharn/...`, etc.), i.e. copies PHARN installed into this CLI's own test apps, not this repo's dev-loop grillers. The counter does not exclude the local test-app dirs, so it over-reports. This does not affect this increment (it is a dev-loop tooling quirk, not a defect in the plan), but the same over-count likely appears in `count-verifiers.mjs` at `/pharn-dev-verify` — noted so it is expected there. Recorded as prose (no `PLAN.md:line` to cite → not a finding-shape object). + +## Summary + +The plan is a tightly-scoped, single-axis mechanical rename that also updates all of its own user-facing documentation. The guarantee audit reduces its real claims to the floor (`npm pack`, `npm run check`) and labels the rest advisory; the trust surface is untouched; the three live-state contradictions (phantom `--wizard`, `provenance` mis-description, CHANGELOG layout) were surfaced and human-resolved before build. The only concerns are two **minor, advisory** ones: (1) the src copy rename is not pinned by a test, and (2) "identical to the scoped publish" is a reasoning claim, not a fetched-tarball diff. Neither blocks build. + +## Verdict + +ADVISORY VERDICT: 2 concerns raised (0 blocking-severity, 2 minor/advisory) — for the human to weigh before `/pharn-dev-build`. This grill did NOT "pass" the plan and guarantees nothing; it surfaces the above. The only deterministic backstops remain `/pharn-dev-build`'s floor-gates (spec-hash, unresolved HALT questions) and `.dev/floor/validate.mjs`. diff --git a/.dev/features/canonical-npm-name/PLAN.md b/.dev/features/canonical-npm-name/PLAN.md new file mode 100644 index 0000000..acb6040 --- /dev/null +++ b/.dev/features/canonical-npm-name/PLAN.md @@ -0,0 +1,119 @@ +# PLAN — canonical-npm-name (rename npm package `@pharn-dev/pharn` → `pharn`) + +- spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 (ARCHITECTURE.md) +- increment: Publish the package under the canonical **unscoped** name `pharn` by setting `package.json.name` and aligning every npm-name reference in the docs/repo surface — no version bump, no CLI behavior change. +- layer(s): packaging (`package.json`) + docs/repo-surface (`*.md`, `docs/**`, `.github/ISSUE_TEMPLATE/**`) — **no `src/` code-behavior layer** (ARCHITECTURE.md §4). See OQ1 for the one src edge case. +- constitution_refs: [P0, P4, P6, P7] + +## Live-state discovery (this run — P6, not from memory) + +- `package.json`: `"name": "@pharn-dev/pharn"` ✓, `"version": "0.2.0"` ✓ (preconditions match). +- `npm view pharn` → **E404 Not Found** ✓ — unscoped name is free on the registry. +- git tag `v0.2.0`: **does not exist** (the repo has **no tags at all**). Nothing to create/move/delete. +- `dist/` is git-ignored (`.gitignore:2`) and untracked; it is rebuilt at pack time by `prepack` → the published tarball = `package.json` + freshly-built `dist/index.js`. +- `.github/workflows/publish.yml` publishes `package.json.name` (no hard-coded name) and runs `npm publish --provenance --access public`, which **deliberately overrides** `publishConfig.provenance:false` (its comment says so). → the workflow needs **no** edit, and `provenance:false` is intentional (see FYI-A). +- `CLAUDE.md:7` already reads "Published on npm as `pharn`" — **already canonical, no change**. +- Trusted docs (`CONSTITUTION/ARCHITECTURE/THREAT-MODEL/LIMITS.md`) contain **no** `@pharn-dev/pharn`; they are write-protected at the floor (`protect-trusted-paths.cjs`) — **not touched**. + +## Classification of every `pharn-dev` hit (task discovery step 3) + +**Category A — npm package name `@pharn-dev/pharn` → becomes `pharn` (change):** + +| File:line | Context | +| --- | --- | +| `package.json:2` | `"name"` — the core change | +| `README.md:9` | npm version badge (`shields.io/npm/v/…`) + `npmjs.com/package/…` link | +| `README.md:16`, `:41` | install commands (`npx … init`) | +| `README.md:54` | prose "The npm package is `@pharn-dev/pharn`" | +| `SECURITY.md:13`, `:20`, `:78` | package link + `npx`/`npm i -g` invocation examples | +| `.github/ISSUE_TEMPLATE/bug_report.md:17`, `:39` | `npx …` version / install-method hints | +| `docs/contributing.md:30`, `:33` | install cmd + "published package" prose | +| `docs/getting-started.md:25`, `:28`, `:38` | package name prose + install cmds | +| `docs/troubleshooting.md:27`, `:68` | re-run / debug install cmds | +| `CHANGELOG.md:35`, `:39` | historical rename bullet + its install cmd — **see OQ2** (treatment differs) | +| `src/index.ts:13` (USAGE) | help-text install hint — **see OQ1** (bundled into `dist`) | +| `src/steps/prereqs.ts:11` | git-missing error hint — **see OQ1** (bundled into `dist`) | + +**Category B — GitHub org paths → MUST NOT change (leave):** every `github.com/pharn-dev/pharn-cli` +(repository/bugs/homepage in `package.json`, README/SECURITY/CHANGELOG badges & links, +`.github/ISSUE_TEMPLATE/config.yml`) and every `pharn-dev/pharn-oss` reference (the degit source repo, +in `CLAUDE.md`, `CONSTITUTION.md`, `LIMITS.md`, `THREAT-MODEL.md`, `README.md`, `docs/**`, +`src/lib/constants.ts`, `src/commands/*.ts`, `tests/**`). These are repo coordinates, not the npm name. + +**Category C — noise (leave):** `/pharn-dev-*` slash-command references throughout `.claude/commands/**` +and `.dev/features/**`; the `pharn-dev/pharn` fragments inside `.claude/settings.local.json` +permission strings and historical `.dev/features/*/PLAN.md`/`GRILL.md` prose. + +## Files (proposed edits — Category A only) + +- `package.json` — line 2 `"name": "@pharn-dev/pharn"` → `"pharn"`. **Nothing else touched** (repository, bugs, homepage, bin, files, engines, publishConfig all unchanged). +- `README.md` — badge (9), install cmds (16, 41), prose (54): `@pharn-dev/pharn` → `pharn`. **Install command is `npx pharn@latest init` — no `--wizard` (OQ3 resolved).** +- `SECURITY.md` — lines 13, 20, 78: `@pharn-dev/pharn` → `pharn` (each occurrence). +- `.github/ISSUE_TEMPLATE/bug_report.md` — lines 17, 39: `npx @pharn-dev/pharn` → `npx pharn`. +- `docs/contributing.md` — lines 30, 33: `@pharn-dev/pharn` → `pharn`. +- `docs/getting-started.md` — lines 25, 28, 38: `@pharn-dev/pharn` → `pharn`. +- `docs/troubleshooting.md` — lines 27, 68: `@pharn-dev/pharn` → `pharn`. +- `CHANGELOG.md` — **(OQ2 resolved)** reconcile the rename bullet (35: `→ pharn`; 39: `npx pharn@latest init`) **and** add the clarifier "Initially published to npm as `@pharn-dev/pharn` (now deprecated); the canonical name is `pharn`." +- `src/index.ts` — **(OQ1 resolved: edit)** USAGE (line 13) `npx @pharn-dev/pharn init` → `npx pharn init`. Copy-only (no control-flow); rebuilds `dist/index.js`. +- `src/steps/prereqs.ts` — **(OQ1 resolved: edit)** git-missing error (line 11) `npx @pharn-dev/pharn init` → `npx pharn init`. Copy-only (no control-flow); rebuilds `dist/index.js`. + +## Contracts satisfied + +- None. This increment touches no `pharn-contracts/*` schema and no inter-layer boundary — it is a packaging-name + docs alignment. (P3: `package.json` is the packaging axis; docs are the doc axis; no code-behavior axis changes.) + +## Evals to write (P1) + +- **None required.** No Capability, `rule_id`, griller, or lens is added, and (default OQ1) no runtime behavior changes → P1 is satisfied vacuously. The existing `tests/index.test.ts` `--help` assertion checks `toContain('Usage:')`, **not** the package-name string, so it stays green whether or not the USAGE string changes. No `.cjs`/`.mjs` floor file is modified → the `node --test` coverage gate does not trigger. Regression is covered by re-running `npm run check`. + +## Guarantee audit (P0) + +- "Published tarball is `pharn-0.2.0.tgz`, **same file list** as the scoped publish; content diffs = `package.json.name` **+ the bundled help/error strings in `dist/index.js`** (OQ1 resolved: edit src)" → **floor**: `npm pack --dry-run` output (deterministic file list + tarball name), captured in the PR body. +- "The repo still builds and all gates pass" → **floor**: `npm run check` (format:check + lint + typecheck + test — deterministic exit codes). +- "Docs/help now name the canonical package `pharn`" → **advisory** (doc/code coherence, P4): backstopped only by `markdownlint`/`prettier` shape gates; textual correctness is human-reviewed at the gate. +- **No new guarantee is made over fetched/untrusted content.** `lib/validate.ts`, `safeJoin`, the degit fetch, and the network guards are **untouched** → P2 surface unchanged. + +## Trust audit (P2) + +- N/A — the increment ingests **no** untrusted artifact (no manifest, no `module.json`, no degit content). It edits static in-repo files + one `package.json` field. No taint is introduced or propagated. THREAT-MODEL.md / LIMITS.md are therefore not engaged by this increment. + +## Determinism audit (P5) + +- N/A — no new branch or classification is added. The only decision points are the open questions below, whose terminal fallback is **ask the human at this gate** (P6-compliant). + +## FYI (noted discrepancies — not blocking) + +- **FYI-A — `publishConfig.provenance`:** the task text says keep `publishConfig { … "provenance": true }` "untouched", but live value is `false` — and `publish.yml` intentionally passes `--provenance` to override it for CI while keeping local `npm publish` working. Action: **leave `publishConfig` untouched** (provenance stays `false`), which satisfies the dominant "untouched" instruction. Flagged only because the task's parenthetical value was inaccurate. +- **FYI-B — CHANGELOG layout:** the `@pharn-dev/pharn` rename is under `## [Unreleased] › ### Changed`; the `## [0.2.0] — 2026-06-11` heading is a *different, older* (module-wizard) release. "Under 0.2.0" is therefore read as the Unreleased rename bullet (lines 35–39), not the dated 0.2.0 section. + +## Resolved decisions (approved by human at GATE 1, 2026-07-22) + +- **OQ1 → Update src strings too.** Edit `src/index.ts` USAGE + `src/steps/prereqs.ts` error; `dist/index.js` rebuilds. Pack check relaxed to "same file list; content diffs = name + help/error strings." +- **OQ2 → Reconcile bullet + clarifier** in `CHANGELOG.md`. +- **OQ3 → `npx pharn@latest init`** (no `--wizard`; the flag was removed). + +The original open-question reasoning is retained below for the record. + +### Open questions (as raised — now resolved above) + +- **OQ1 — the two `src/*.ts` help/error strings (pack-identity vs. full coherence).** `src/index.ts` + USAGE and `src/steps/prereqs.ts` embed `npx @pharn-dev/pharn init`; both are bundled into + `dist/index.js`. The task simultaneously says (a) "every `@pharn-dev/pharn` must become `pharn`" and + (b) the packed tarball must be "identical to the scoped publish except the name field." These + **conflict**: editing the src strings changes `dist/index.js` too, so the tarball would differ by more + than `package.json.name`. **Default/recommended:** leave the two src strings this increment (dist stays + byte-identical; `@pharn-dev/pharn` still resolves, so `--help`/error text is non-canonical but not + broken), ship the string cleanup in the next version bump. Alternative: edit them now and relax the + pack-identity check to "same file list, content diffs = name + help string." +- **OQ2 — CHANGELOG treatment.** Recommended: reconcile the rename bullet to the canonical outcome + (35: `→ pharn`; 39: `npx pharn@latest init`) **and** add the clarifier "Initially published to npm as + `@pharn-dev/pharn` (now deprecated); the canonical name is `pharn`." Alternative: add **only** the + clarifier line and leave the historical bullet verbatim (keeps a literal record that the scoped name + was published, at the cost of an internally mixed bullet). +- **OQ3 — README install command / the `--wizard` premise is stale.** The task says the canonical UX is + `npx pharn@latest init --wizard` and that the README must read exactly that. **But `--wizard` is not a + real flag:** `src/index.ts` declares no `wizard` boolean, `init` takes no options and runs + `runInitArchetype()` unconditionally, and CLAUDE.md + `init.ts:29-31` state the wizard flow was + **removed entirely** (`--wizard` would parse as a no-op, documenting a phantom flag → P4 violation). + The README currently has **no** `--wizard`. **Recommended:** README install stays `npx pharn@latest init` + (truthful). Alternative: write `--wizard` exactly as the task says (documents a dead flag — not + recommended), or treat re-adding a real `--wizard` as a separate increment. diff --git a/.dev/features/canonical-npm-name/REGRESSION.md b/.dev/features/canonical-npm-name/REGRESSION.md new file mode 100644 index 0000000..d2896d9 --- /dev/null +++ b/.dev/features/canonical-npm-name/REGRESSION.md @@ -0,0 +1,54 @@ +# REGRESSION — canonical-npm-name + +- **Base:** `HEAD` (working-tree dogfood build; `git status --porcelain` non-empty → base = HEAD). +- **Verdict (floor, `check-regress.mjs verdict`):** `no-regressions` — no deterministically-detectable breakage outside the feature. + +## Inside (the changed scope — 10 files, all declared in PLAN `## Files`) + +`.github/ISSUE_TEMPLATE/bug_report.md`, `CHANGELOG.md`, `README.md`, `SECURITY.md`, +`docs/contributing.md`, `docs/getting-started.md`, `docs/troubleshooting.md`, `package.json`, +`src/index.ts`, `src/steps/prereqs.ts`. + +`scope` (fix #7) confirmed `escaped: []` — the build wrote nothing outside its declared `## Files`. + +## Outside gates (base → head exit codes) + +| Gate | base | head | classification | +| --- | --- | --- | --- | +| `tests` (`node --test` over 44 floor/hook `*.test.mjs`/`*.test.cjs`) | 1 | 1 | **pre-existing** | +| `validate` (`node .dev/floor/validate.mjs .`, whole-repo) | 1 | 1 | **pre-existing** | + +- `regressions[]`: **(none)** +- `pre_existing[]`: `tests`, `validate` + +## Why both gates are pre-existing (not caused by the rename) + +Both gates are RED at the **baseline** (the 10 files reverted to HEAD) as well as at HEAD, so +`check-regress.mjs` excludes them as pre-existing (`base != 0` is never blamed on the feature). The +red is entirely the **gitignored `test-*/` install dirs** present in the working tree: + +- `validate.mjs .` flags deliberately-red fixtures under `test-*/pharn/floor/test-fixtures/red/`. +- the `tests` gate's 3 failures are all in `lens-scanner-map.test.mjs`, which scans the **live** tree + and trips on the lenses/scanners installed inside `test-*/`. + +Neither depends on the 10 renamed files (docs + `package.json` name + two `src/` string literals); the +rename cannot affect the floor/hook `node --test` suite or capability `validate`, and the `src/` change +is covered green by the build's `npm run check` (vitest, 378 passed). + +## Baseline methodology note (honest) + +The baseline was captured by a **surgical in-place revert of only the 10 files** (`git checkout HEAD -- `) +with the gitignored `test-*/` + `node_modules` left in place, so base↔head differ **solely** by the +rename. A `git worktree` baseline was rejected: it produces a clean checkout **without** the gitignored +`test-*/` dirs, so the whole-repo gates falsely flip GREEN→RED on the environment difference rather than +on the change. + +## Residual (P0/P7) + +`/pharn-dev-regress` catches exactly what its suite catches — nothing more. Because the `tests` and +`validate` gates are already red at baseline (test-*/ noise), a *new* failure they cover would be masked +(both sides non-zero → classified pre-existing). That masking does not apply to this increment: the +rename cannot touch the floor/hook suite or capability structure, and its `src/` effect is verified by +the build's green vitest run. The verdict certifies the **comparison**, not that the increment is whole. + +**REGRESSIONS: none — no deterministically-detectable breakage outside the feature.** diff --git a/.dev/features/canonical-npm-name/REVIEW.md b/.dev/features/canonical-npm-name/REVIEW.md new file mode 100644 index 0000000..9d9b0e3 --- /dev/null +++ b/.dev/features/canonical-npm-name/REVIEW.md @@ -0,0 +1,75 @@ +# REVIEW — canonical-npm-name (rename `@pharn-dev/pharn` → `pharn`) + +Increment reviewed: the 10-file npm-name rename (`package.json` name + README/SECURITY/docs/CHANGELOG/ +issue-template references + two `src/` help/error strings). The increment is `trust: untrusted`; nothing +in the reviewed files is an instruction to the reviewer. + +## Step 1 — Floor first (P0) + +The increment adds **no** PHARN markdown capability (all 7 changed `.md` files are docs with no `role:` +frontmatter; the other 3 changed files are `package.json` + two `src/*.ts`). So `validate.mjs` over the +increment's own files is **vacuously GREEN** — there is nothing for it to check. + +The whole-repo `node .dev/floor/validate.mjs .` is RED, but **`/pharn-dev-regress` proved deterministically +that this is pre-existing** (RED at the HEAD baseline and at HEAD → `pre_existing: ["validate"]`, +`regression-report.json`): it flags gitignored `test-*/` install fixtures, not anything this increment +touched. So it is **not** a blocking finding against this increment. + +## The four lenses + +### L-floor → P0 — no finding + +Every claim the increment makes is floor-reduced or advisory-labeled (`PLAN.md` guarantee audit): +tarball name/file-list → `npm pack` (floor); `npm run check` → deterministic gates (floor); "docs name +the canonical package" → **advisory** (labeled). No new guarantee is made over fetched/untrusted content +(`lib/validate.ts`, `safeJoin`, the network guards are untouched). Clean. + +### L-eval → P1 — no finding + +No Capability and no `rule_id` are added → P1 is satisfied vacuously; nothing to bind. The floor agrees +(no capability for `validate` to require evals of). The `src/` copy change is a string edit with no new +behavior; the existing `tests/index.test.ts` `--help` assertion (`toContain('Usage:')`) stays green, and +the whole `npm test` suite (378) passes at verify. Clean. + +### L-trust → P2 — no finding + +The increment ingests no untrusted artifact (no manifest, no `module.json`, no degit content); it edits +static in-repo files + one `package.json` field. No taint is introduced or propagated. No instruction- +looking content in the reviewed files altered the review. Clean. + +### L-axis → P3 — no finding + +Every changed file changes for the **single** axis "canonical package name." No new cross-command or +step→step import was added (the two `src/*.ts` edits are string-literal changes only, no new imports). +Clean. + +## Gates (fix #3) + +- **floor-gate (blocking):** none. +- **advisory-gate (warn):** none against the increment. (The chain's standing `validate` / verify FAIL is + the pre-existing whole-repo `test-*/` contamination, not a defect in this increment — surfaced honestly, + owned by the human at the post-review gate.) + +## Verdict + +**GREEN (advisory)** — a clean, single-axis mechanical rename with all increment-relevant gates green +(`test`, `lint`, `format:check`, `lint:md`) and no floor or advisory findings. Not a certification of +anything beyond what those gates check (P0). + +## Proposed lessons (candidates only — NOT written to canon here; a human-gated `/pharn-dev-memory-promote` run decides) + +Provenance: increment `canonical-npm-name`, this run (2026-07-22). These arose from the pipeline tooling +during this run, not from a defect in the increment — offered because they are **real, recurring** risks +for this repo's dev loop (P7): + +1. **The `/pharn-dev-regress` `git worktree` baseline is blind to gitignored dirs.** This repo gitignores + the `test-*/` install trees, which the whole-repo gates (`validate`, `lens-scanner-map.test.mjs`) scan. + A detached-worktree baseline lacks them, so those gates falsely flip GREEN→RED on the *environment* + difference, not the change. A faithful baseline for a working-tree dogfood build must revert only the + feature's files **in place** (leaving gitignored dirs present), not use a clean worktree. (Cost me a + false "regression" reading this run.) +2. **`validate.mjs .` / `count-grillers.mjs .` over-report from the gitignored `test-*/` install dirs** + (81 grillers, 15 red fixtures — all under `test-*/`). Consider scoping the floor tooling to exclude the + local install fixtures, so the whole-repo `validate` gate reflects the source tree, not installed + copies. This is what forces the build/verify `validate` verdict RED on every increment regardless of + its content. diff --git a/.dev/features/canonical-npm-name/SHIP.md b/.dev/features/canonical-npm-name/SHIP.md new file mode 100644 index 0000000..577b1e8 --- /dev/null +++ b/.dev/features/canonical-npm-name/SHIP.md @@ -0,0 +1,48 @@ +# SHIP — canonical-npm-name (advisory roll-up) + +Increment: rename the npm package `@pharn-dev/pharn` → `pharn` (canonical unscoped name), no version +bump, no behavior change. Run via `/pharn-dev-ship` (gated mode). + +## Stages that ran, in order, and where the run ended + +`plan → grill → build → regress → verify → review` — all ran. **The run ends here, at GATE 2** (the +post-review human decision: merge / fix / abandon). `/pharn-dev-ship` does not merge, push, or seal. + +## Structural verdicts read (verbatim) + +| Stage | Verdict source | Value | +| --- | --- | --- | +| plan | human approval halt (GATE 1) | **Approved** (2026-07-22), with OQ1/OQ2/OQ3 resolved | +| grill | advisory (gates nothing) | 2 concerns, 0 blocking (2 minor/advisory) | +| build | `npm run check` (build skill's floor) | **GREEN** (format ✓ lint ✓ typecheck ✓ 378 tests ✓) | +| build | `.dev/floor/validate.mjs .` exit (ship orchestrator's build-verdict read) | **1** — pre-existing `test-*/` contamination; human authorized continue | +| regress | `regression-report.json` `.verdict` | **`no-regressions`** (exit 0) | +| verify | `verify-report.json` `.verdict` | **`FAIL`** — `failing_gates: ["validate"]` only | +| review | advisory (no structural verdict) | **GREEN (advisory)**, no findings — see `REVIEW.md` | + +## The single standing failure, characterized honestly + +Every failing signal in this run is the **one** pre-existing condition: `node .dev/floor/validate.mjs .` +walks the **gitignored `test-*/` install dirs** and flags 15 deliberately-red test fixtures there. This: + +- is **not** caused by the rename — `/pharn-dev-regress` proved it deterministically (`validate` RED at the + HEAD baseline **and** at HEAD → `pre_existing`, not a regression); +- leaves **every rename-relevant gate GREEN** — build `npm run check` (incl. vitest over `src/`), and + verify's `test` / `lint` / `format:check` / `lint:md` all exit 0; +- is a repo-tooling granularity limit (the floor tooling does not exclude the local install fixtures), + surfaced for the human, not a defect in the increment. See `REVIEW.md` "Proposed lessons". + +## Pointers (cited, not restated — P4) + +- `REVIEW.md` — 4 advisory lenses, GREEN, no findings; 2 proposed dev-loop lessons. +- `GRILL.md` — advisory, 2 minor concerns (untested copy string; "identical to scoped publish" is a + reasoning claim, not a fetched-tarball diff). +- `VERIFY.md` / `verify-report.json`, `REGRESSION.md` / `regression-report.json` — the floor verdicts above. + +## Standing decision is the human's + +The chain ran; the named floor verdicts are as shown — **this is NOT a judgment that the increment is +good or wise; that is the human's call at the post-review gate.** Note: all npm-side actions (publish, +deprecating `@pharn-dev/pharn`, Trusted Publisher config) are the human's, post-merge, per the increment's +intent. The working tree holds exactly the 10-file rename diff (+ these pipeline artifacts under +`.dev/features/canonical-npm-name/`); nothing is committed or pushed. diff --git a/.dev/features/canonical-npm-name/VERIFY.md b/.dev/features/canonical-npm-name/VERIFY.md new file mode 100644 index 0000000..166ad28 --- /dev/null +++ b/.dev/features/canonical-npm-name/VERIFY.md @@ -0,0 +1,45 @@ +# VERIFY — canonical-npm-name + +**VERIFY FAILS: gate `validate` red — stage FAILS** (`check-verify.mjs` exit 1). **But the sole failing +gate is the pre-existing whole-repo `validate` contamination, not the rename** — see below. + +## Floor gates (the verdict owner) + +| Gate | exit | | +| --- | --- | --- | +| `test` (`npm test`, vitest — 378 tests incl. the feature's `src/`) | 0 | ✅ | +| `lint` (`npm run lint`, eslint) | 0 | ✅ | +| `format:check` (prettier) | 0 | ✅ | +| `lint:md` (markdownlint) | 0 | ✅ | +| `validate` (`node .dev/floor/validate.mjs .`, whole-repo) | **1** | ❌ pre-existing | + +`failing_gates: ["validate"]`. + +## Why `validate` is red (pre-existing, not this increment) + +`validate.mjs .` walks the **whole repo**, including the **gitignored `test-*/` install dirs**, and flags +15 deliberately-red fixtures under `test-*/pharn/floor/test-fixtures/red/skill.md`. These are committed- +into-the-install test fixtures that exist regardless of this rename: + +- `/pharn-dev-regress` proved it deterministically: `validate` is **RED at the baseline** (the 10 files + reverted to HEAD) **and** at HEAD → classified **pre-existing**, not a regression (`regression-report.json`). +- This increment added **no** PHARN markdown capability and touched **no** `test-*/` dir, so `validate`'s + verdict on those fixtures is byte-identical before/after the rename. + +Every gate that actually exercises the rename — `test` (vitest over `src/`), `lint`, `format:check`, +`lint:md` — is **GREEN**. The rename is clean; the FAIL is a whole-repo `validate` granularity artifact of +the gitignored `test-*/` install tree (the same root cause as `count-grillers.mjs` over-reporting 81 from +`test-*/`). + +## Verifiers (advisory layer) + +`node .dev/floor/count-verifiers.mjs .` → `{"registered":0,"verifiers":[]}` — **no verifiers registered — +floor gates only.** No advisory annotations. + +## Residual (P0/P7) + +Verified = the named gates passed; this is NOT a guarantee of correctness beyond what those gates check — +verifier concerns are advisory help, not assurance. Here the verdict is FAIL, owned solely by the whole- +repo `validate` gate's pre-existing `test-*/` contamination; the increment-relevant gates all pass. The +floor cannot, by itself, scope `validate` to exclude the gitignored install fixtures — that is a +repo-tooling limitation surfaced honestly for the human, not a defect in the rename. diff --git a/.dev/features/canonical-npm-name/regression-report.json b/.dev/features/canonical-npm-name/regression-report.json new file mode 100644 index 0000000..10d18b7 --- /dev/null +++ b/.dev/features/canonical-npm-name/regression-report.json @@ -0,0 +1,31 @@ +{ + "base": "HEAD", + "inside": [ + ".github/ISSUE_TEMPLATE/bug_report.md", + "CHANGELOG.md", + "README.md", + "SECURITY.md", + "docs/contributing.md", + "docs/getting-started.md", + "docs/troubleshooting.md", + "package.json", + "src/index.ts", + "src/steps/prereqs.ts" + ], + "outside_gates": { + "tests": { + "base": 1, + "head": 1 + }, + "validate": { + "base": 1, + "head": 1 + } + }, + "regressions": [], + "pre_existing": [ + "tests", + "validate" + ], + "verdict": "no-regressions" +} diff --git a/.dev/features/canonical-npm-name/verify-report.json b/.dev/features/canonical-npm-name/verify-report.json new file mode 100644 index 0000000..418fe22 --- /dev/null +++ b/.dev/features/canonical-npm-name/verify-report.json @@ -0,0 +1,18 @@ +{ + "feature": "canonical-npm-name", + "gates": { + "format:check": 0, + "lint": 0, + "lint:md": 0, + "test": 0, + "validate": 1 + }, + "verdict": "FAIL", + "failing_gates": [ + "validate" + ], + "verifiers": { + "registered": 0, + "findings": [] + } +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3ad8245..ead9e8e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ A clear description of the bug. - **Command**: - **Step / area**: -- **`pharn` version** (`npx @pharn-dev/pharn --version`, or `pharnVersion` in `pharn.config.json`): +- **`pharn` version** (`npx pharn --version`, or `pharnVersion` in `pharn.config.json`): - **Skills version** (`.claude/SKILLS_VERSION`, or `skillsVersion` in `pharn.config.json`): ## Steps to reproduce @@ -36,7 +36,7 @@ What actually happened. Paste the relevant CLI output (run with `PHARN_DEBUG=1` - Node.js version (`node --version`): - npm version (`npm --version`): - OS: -- Install method: +- Install method: - Target project: ## Additional context diff --git a/.pharn/pharn-dev-regress/base-results.json b/.pharn/pharn-dev-regress/base-results.json index e992050..1e84650 100644 --- a/.pharn/pharn-dev-regress/base-results.json +++ b/.pharn/pharn-dev-regress/base-results.json @@ -1 +1 @@ -{"tests":1} +{"tests":1,"validate":1} diff --git a/.pharn/pharn-dev-regress/head-results.json b/.pharn/pharn-dev-regress/head-results.json index e992050..1e84650 100644 --- a/.pharn/pharn-dev-regress/head-results.json +++ b/.pharn/pharn-dev-regress/head-results.json @@ -1 +1 @@ -{"tests":1} +{"tests":1,"validate":1} diff --git a/.pharn/pharn-dev-verify/results.json b/.pharn/pharn-dev-verify/results.json index d76b920..dc0f388 100644 --- a/.pharn/pharn-dev-verify/results.json +++ b/.pharn/pharn-dev-verify/results.json @@ -1 +1 @@ -{"test":0,"validate":0,"lint":0,"format:check":0,"lint:md":0} +{"test":0,"validate":1,"lint":0,"format:check":0,"lint:md":0} \ No newline at end of file diff --git a/.pharn/writes-scope.json b/.pharn/writes-scope.json index 6f1b0d8..c2dd576 100644 --- a/.pharn/writes-scope.json +++ b/.pharn/writes-scope.json @@ -1,10 +1,7 @@ { "scope": [ - "README.md", - "docs/commands/init.md", - "docs/getting-started.md", - "docs/contributing.md" + ".dev/features/canonical-npm-name/SHIP.md" ], - "set_by": "manual-adhoc (user-authorized in chat: update README.md + fix stale legacy-fallback / two-bin doc claims to the archetype model)", - "set_at": "2026-07-22T13:58:59.025Z" + "set_by": ".claude/commands/pharn-dev-ship.md", + "set_at": "2026-07-22T17:36:31.136Z" } diff --git a/CHANGELOG.md b/CHANGELOG.md index c1fa8d1..ec9e8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,11 +32,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Renamed the npm package `pharn-cli` → `@pharn-dev/pharn`** and made it publish-ready — added `repository`, +- **Renamed the npm package `pharn-cli` → `pharn`** and made it publish-ready — added `repository`, `bugs`, `homepage`, `keywords`, and `publishConfig` (public access + provenance); dropped the `pharn-cli` bin alias for a single `pharn` bin; and added a `prepack` build so `npm publish` always ships a freshly compiled `dist/`. No CLI behavior change and `version` is unchanged; the package now - installs via `npx @pharn-dev/pharn@latest init`. + installs via `npx pharn@latest init`. +- Initially published to npm as `@pharn-dev/pharn` (now deprecated); the canonical name is `pharn`. - Docs: surfaced the new optional `/pharn-spec` stage (intent capture before `/pharn-plan`) in getting-started and the `pharn-pipeline` module description, matching `pharn-oss`. No CLI code change — `/pharn-spec` ships transparently via the existing whole-module install from `main`. diff --git a/README.md b/README.md index 10c7d62..ec1db98 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ The installer for [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code that keeps comprehension debt legible instead of silent. -[![npm](https://img.shields.io/npm/v/@pharn-dev/pharn)](https://www.npmjs.com/package/@pharn-dev/pharn) +[![npm](https://img.shields.io/npm/v/pharn)](https://www.npmjs.com/package/pharn) [![CI](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml) [![CodeQL](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green)](./LICENSE) [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](./package.json) ```bash -npx @pharn-dev/pharn init +npx pharn init ``` @@ -38,7 +38,7 @@ npx @pharn-dev/pharn init ## Install ```bash -npx @pharn-dev/pharn@latest init +npx pharn@latest init ``` `pharn` runs straight from npm — `npx` fetches the latest published version and runs it in your project; no global install. Requires a git-initialized project and Node >= 20. @@ -51,7 +51,7 @@ Vibe-coding with an AI agent is fast — until the chat history scrolls away and `pharn` is how you get it. Run `pharn init` in your project; the CLI detects your project's **archetype(s)**, fetches the applicable PHARN **capabilities** from `pharn-dev/pharn-oss`, copies them plus the canonical constitution into the mirrored layout (`.claude/` + `pharn/`), and writes `pharn.config.json`. -> The npm package is `@pharn-dev/pharn`; it installs a single `pharn` binary. +> The npm package is `pharn`; it installs a single `pharn` binary. --- diff --git a/SECURITY.md b/SECURITY.md index e4397a1..06efa96 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,14 +10,14 @@ The CLI's security model is **deterministic, not model-driven**: it never asks a ## Supported versions -`pharn` is published to npm as [`@pharn-dev/pharn`](https://www.npmjs.com/package/@pharn-dev/pharn) and is typically run via `npx @pharn-dev/pharn init`. We patch security issues against the **latest** published version only; `npx @pharn-dev/pharn@latest ...` always resolves to a supported release. +`pharn` is published to npm as [`pharn`](https://www.npmjs.com/package/pharn) and is typically run via `npx pharn init`. We patch security issues against the **latest** published version only; `npx pharn@latest ...` always resolves to a supported release. | Version | Supported | | -------- | ------------------ | | Latest | :white_check_mark: | | < Latest | :x: | -Because the CLI is normally invoked through `npx`, most users run the latest version automatically. If you have a pinned or globally installed copy, update it (`npm i -g @pharn-dev/pharn@latest`) or invoke `npx @pharn-dev/pharn@latest` to pick up fixes. +Because the CLI is normally invoked through `npx`, most users run the latest version automatically. If you have a pinned or globally installed copy, update it (`npm i -g pharn@latest`) or invoke `npx pharn@latest` to pick up fixes. ## Reporting a vulnerability @@ -75,7 +75,7 @@ Scope follows the surface described above: everything that touches remote input 1. **Run it in an existing, version-controlled project** so you can diff exactly what `init` wrote (`.claude/` and `pharn.config.json`) before committing. 2. **Review the vendor skills** the wizard offers before accepting them — vendor selection is opt-in and nothing is selected by default; only accept vendors you recognize. -3. **Prefer `npx @pharn-dev/pharn@latest`** so you run the current, supported release rather than a stale pinned copy. +3. **Prefer `npx pharn@latest`** so you run the current, supported release rather than a stale pinned copy. 4. **Inspect the cloned `.claude/` skills** before running them through your AI tool — installation fetches remote content. 5. **Set `PHARN_DEBUG=1`** if a manifest or clone step fails unexpectedly, and report anything that looks like the CLI fetching or writing somewhere it should not. diff --git a/docs/contributing.md b/docs/contributing.md index b41c001..5f3cbc4 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -27,10 +27,10 @@ npm install From the `test-app/` directory (which needs its own `package.json`) after `build:install-local`: ```bash -npx @pharn-dev/pharn init +npx pharn init ``` -Published package `@pharn-dev/pharn` exposes a single `pharn` bin (see `package.json`). +Published package `pharn` exposes a single `pharn` bin (see `package.json`). ## Quality gates diff --git a/docs/getting-started.md b/docs/getting-started.md index c27aeba..c279705 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -22,10 +22,10 @@ You can continue after any warning by confirming. ## Running the CLI -The npm package name is **`@pharn-dev/pharn`**. It installs a single command, **`pharn`**. +The npm package name is **`pharn`**. It installs a single command, **`pharn`**. ```bash -npx @pharn-dev/pharn init +npx pharn init ``` ## Quick start @@ -35,7 +35,7 @@ npx create-next-app@latest my-app cd my-app npx shadcn@latest init git init && git add -A && git commit -m "init" -npx @pharn-dev/pharn init +npx pharn init ``` `pharn` with no subcommand runs `init` (same as `pharn init`). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 80e18e5..f08c67d 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -24,7 +24,7 @@ The CLI message says "git not found" but the check is for a **`.git` directory** ```text ✗ git not found. Run: git init && git add -A && git commit -m 'init' - Then re-run: npx @pharn-dev/pharn init + Then re-run: npx pharn init ``` Exits with code **1**. @@ -65,7 +65,7 @@ Symptoms: Causes include a degit clone failure (network/GitHub), or a selected capability missing at its expected path (`//.md`) in the fetched repo. Set `PHARN_DEBUG=1` and re-run for the full stack trace: ```bash -PHARN_DEBUG=1 npx @pharn-dev/pharn init +PHARN_DEBUG=1 npx pharn init ``` ## Overwrite declined diff --git a/package.json b/package.json index 1b2f0ff..1a3ef80 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@pharn-dev/pharn", + "name": "pharn", "version": "0.2.0", "description": "Audit-grade AI development methodology for Claude Code — spec, plan, grill, build, verify, ship.", "keywords": [ diff --git a/src/index.ts b/src/index.ts index e9e930c..dfde9d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { runList } from './commands/list.js'; import { runStatus } from './commands/status.js'; import { PHARN_VERSION } from './version.js'; -const USAGE = `Pharn - Installs PHARN (an audit-grade methodology for Claude Code) into your project. npx @pharn-dev/pharn init detects your project's archetype and installs the applicable PHARN capabilities; pharn add installs another capability later; pharn update bumps to the latest skills version. +const USAGE = `Pharn - Installs PHARN (an audit-grade methodology for Claude Code) into your project. npx pharn init detects your project's archetype and installs the applicable PHARN capabilities; pharn add installs another capability later; pharn update bumps to the latest skills version. Usage: pharn [command] [options] diff --git a/src/steps/prereqs.ts b/src/steps/prereqs.ts index 221c024..c9a7515 100644 --- a/src/steps/prereqs.ts +++ b/src/steps/prereqs.ts @@ -8,7 +8,7 @@ export function runGitPrereq(): void { if (!existsSync(resolve(cwd, '.git'))) { fail( - "✗ git not found.\n Run: git init && git add -A && git commit -m 'init'\n Then re-run: npx @pharn-dev/pharn init", + "✗ git not found.\n Run: git init && git add -A && git commit -m 'init'\n Then re-run: npx pharn init", ); } } From 7b84868ec558128b19dd920f2717f1bbdc518361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 22:19:30 +0200 Subject: [PATCH 05/10] ci(publish): use npm Trusted Publishing (OIDC), drop NPM_TOKEN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert publish.yml to pure OIDC Trusted Publishing — no secrets in the workflow: - delete the env NODE_AUTH_TOKEN / NPM_TOKEN block (auth is now OIDC) - gate the publish job behind the `npm-publish` environment - bump Node to 24 and install npm@latest (Trusted Publishing needs npm >= 11.5.1) - drop the workflow_dispatch trigger (releases only) - guard: the release tag must equal package.json version before publish Requires an out-of-band `npm-publish` GitHub environment and an npmjs.org Trusted Publisher for publish.yml + that environment; the first publish stays a manual local `npm publish`. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/publish.yml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2b74c6d..7408d77 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,36 +1,46 @@ name: publish -# Publish to npm when a GitHub Release is published, or on manual dispatch. +# Publish to npm when a GitHub Release is published. # Cut a release (tag vX.Y.Z, matching package.json "version") and this runs. +# +# Auth = npm Trusted Publishing (OIDC) — configured on npmjs.org as a Trusted +# Publisher for this workflow (publish.yml) + environment `npm-publish`. No +# secrets or tokens appear anywhere in this workflow. The local FIRST publish +# stays manual (`npm publish`, run once by a maintainer to create the package +# before the Trusted Publisher can take over). on: release: types: [published] - workflow_dispatch: permissions: contents: read - id-token: write # required for npm provenance (Sigstore attestation via OIDC) + id-token: write # required for OIDC: npm Trusted Publishing + provenance attestation jobs: publish: runs-on: ubuntu-latest + environment: npm-publish steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6 with: - node-version: 20 + node-version: 24 cache: npm registry-url: https://registry.npmjs.org + - name: Update npm # Trusted Publishing requires npm >= 11.5.1 + run: npm install -g npm@latest - name: Install run: npm ci + - name: Verify tag matches package.json version + run: | + TAG="${GITHUB_REF_NAME#v}" + PKG="$(node -p "require('./package.json').version")" + [ "$TAG" = "$PKG" ] || { echo "tag v$TAG != package.json $PKG"; exit 1; } # `npm publish` runs prepublishOnly (npm run check) + prepack (build) first, - # so a failing gate blocks the release. `--provenance` overrides the - # publishConfig `provenance: false` default: CI gets a signed attestation - # (needs id-token: write + a CI OIDC provider) while local `npm publish` - # still works without one. + # so a failing gate blocks the release. `--provenance` emits a signed + # attestation via the same OIDC id-token used for Trusted Publishing + # (needs id-token: write). Auth itself is OIDC — no token or secret. - name: Publish run: npm publish --provenance --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From db11f65113d3580b07369b47653417a8cc2a1dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 22:26:57 +0200 Subject: [PATCH 06/10] docs(dev-loop): add publish-oidc-trusted audit trail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PHARN dev-loop record for the publish.yml OIDC conversion (commit 7b84868): PLAN, GRILL, REGRESSION, VERIFY, REVIEW, SHIP + the two machine reports. It records the floor verdicts read at each stage — build `validate` RED and verify FAIL were the same pre-existing untracked test-app artifact (CI-equivalent green), proceeded past only by explicit human decision at the build / GATE-2 gates. Co-Authored-By: Claude Opus 4.8 --- .dev/features/publish-oidc-trusted/GRILL.md | 57 ++++++++++++++ .dev/features/publish-oidc-trusted/PLAN.md | 52 +++++++++++++ .../publish-oidc-trusted/REGRESSION.md | 42 ++++++++++ .dev/features/publish-oidc-trusted/REVIEW.md | 77 +++++++++++++++++++ .dev/features/publish-oidc-trusted/SHIP.md | 36 +++++++++ .dev/features/publish-oidc-trusted/VERIFY.md | 32 ++++++++ .../regression-report.json | 22 ++++++ .../publish-oidc-trusted/verify-report.json | 15 ++++ 8 files changed, 333 insertions(+) create mode 100644 .dev/features/publish-oidc-trusted/GRILL.md create mode 100644 .dev/features/publish-oidc-trusted/PLAN.md create mode 100644 .dev/features/publish-oidc-trusted/REGRESSION.md create mode 100644 .dev/features/publish-oidc-trusted/REVIEW.md create mode 100644 .dev/features/publish-oidc-trusted/SHIP.md create mode 100644 .dev/features/publish-oidc-trusted/VERIFY.md create mode 100644 .dev/features/publish-oidc-trusted/regression-report.json create mode 100644 .dev/features/publish-oidc-trusted/verify-report.json diff --git a/.dev/features/publish-oidc-trusted/GRILL.md b/.dev/features/publish-oidc-trusted/GRILL.md new file mode 100644 index 0000000..f9c0875 --- /dev/null +++ b/.dev/features/publish-oidc-trusted/GRILL.md @@ -0,0 +1,57 @@ +# GRILL — publish-oidc-trusted (ADVISORY) + +Plan under interrogation: `.dev/features/publish-oidc-trusted/PLAN.md` +Spec-hash check (content-hash floor primitive, surfaced only): recomputed `sha256(ARCHITECTURE.md)` = `bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e` — **MATCHES** the plan's `spec_content_hash`. No drift finding. (The block on drift is `/pharn-dev-build`'s floor-gate, not this stage.) + +Griller membership (floor): `count-grillers .` returned 81, but every path is a `test-*/` install fixture (pharn-cli is the installer; canonical grillers ship from pharn-oss). The `testability` axis is applied inline below (Layer 1 present/absent + Layer 2 adequacy). + +## Findings (grouped by axis; all advisory — this stage gates nothing) + +### Axis: guarantee-audit completeness (P0) + +```yaml +- type: FINDING # enum-gated (TRUSTED: my own assertion) + rule_id: P0 # enum-gated + severity: minor # enum-gated value; assignment is advisory (fix #3) + file: ".dev/features/publish-oidc-trusted/PLAN.md:34" # resolves + problem: "The 'no secrets' absence-grep is labeled FLOOR and 'Verifiable at build/verify', but no standing gate (npm run check, validate.mjs) actually runs it — it is floor-REDUCIBLE, not floor-ENFORCED (the finding-shape.md distinction). Correctly it is a manual verify-time grep, not an always-on gate." + evidence: "\"**\\\"No secrets appear anywhere in the workflow\\\"** → **FLOOR** (deterministic absence test): `grep -c 'secrets\\.' .github/workflows/publish.yml` == 0. Verifiable at build/verify.\"" +``` + +### Axis: honest scope / smallest increment (P7) + +```yaml +- type: FINDING + rule_id: P7 # enum-gated + severity: minor + file: ".dev/features/publish-oidc-trusted/PLAN.md:18" + problem: "Edit #5 (the tag==package.json version guard) is a release-hygiene concern arguably distinct from the OIDC-auth migration axis — two concerns bundled in one increment. The human's mandate explicitly bundled them; surfaced for awareness, not a block." + evidence: "\"5. **Add a guard step before Publish** — `TAG=\\\"${GITHUB_REF_NAME#v}\\\"; ... [ \\\"$TAG\\\" = \\\"$PKG\\\" ] || { echo ...; exit 1; }`\"" +``` + +### Axis: testability (griller — Layer 1 floor present/absent, Layer 2 advisory adequacy) + +Layer 1 (present/absent, structural): a verification approach **IS present** — the plan's `## Evals to write (P1)` and `## Guarantee audit (P0)` sections declare non-empty, real verification for what it builds (npm run check stays GREEN; a `secrets.` absence-grep; human review + GitHub's parser at release). **No absence finding.** Layer 2 (adequacy, advisory) raises: + +```yaml +- type: FINDING + rule_id: P1 # enum-gated — cited, not restated (P4) + severity: minor + file: ".dev/features/publish-oidc-trusted/PLAN.md:27" + problem: "Adequacy (advisory): the increment's core artifact — the workflow YAML — has NO automated correctness check; the floor only proves the TS product is untouched and that no `secrets.` string remains. YAML validity + 'OIDC actually publishes' rest on human review + GitHub's runtime parser. The plan discloses this honestly, so this is an ACKNOWLEDGED residual, not a hidden gap." + evidence: "\"**NONE — a deliberate, honest exception, not an omission:** ... Verification is therefore: (a) `npm run check` ... (b) a deterministic absence-grep ... (c) human review + GitHub's own workflow parser at release time (out-of-band).\"" +``` + +## Prose summary + +The plan is small, coherent, and unusually honest — its guarantee audit already draws the exact floor/advisory lines this stage exists to check, and the spec-hash matches (no drift). Three **minor** concerns, none blocking: + +1. **P0 precision (PLAN.md:34).** The `secrets.` absence-grep is real and deterministic, but calling it "FLOOR … Verifiable at build/verify" slightly overstates: nothing in the standing floor (`npm run check`, `validate.mjs`) runs a grep over `.github/`. It is *floor-reducible* and I (as `/pharn-dev-verify`) can run it, but it is not an enforced gate. Recommend the build/verify step **actually execute** the grep and record its `0` result, so the FLOOR claim is backed by an observed exit, matching finding-shape.md's "floor-reducible-but-not-yet-enforced" honesty. +2. **P7 bundling (PLAN.md:18).** The tag==version guard is a release-safety concern distinct from the auth migration. It's tiny and the human mandated it together, so bundling is defensible — surfaced only so the human is aware two axes ride in one increment. +3. **Testability adequacy (PLAN.md:27).** The workflow itself is unverified by any floor primitive here; correctness is out-of-band (review + GitHub). The plan says so plainly — good — but the human should weigh that the *real* proof this change works is the **first actual release**, which the plan correctly frames as a manual/operational step (external prerequisites (i)–(iii)). + +One non-finding note folded into prose (not a separate finding): the plan keeps `--provenance` under OIDC. That is valid (provenance uses the same `id-token: write`), but with npm ≥ 11.5.1 Trusted Publishing it is partly redundant; harmless, and outside the six named edits — left as-is per "No other files." + +## Verdict + +**ADVISORY VERDICT: 3 concerns raised (0 blocking-severity, 3 minor) — for the human to weigh before `/pharn-dev-build`.** This grill-log gates nothing; the deterministic backstops remain `/pharn-dev-build`'s floor-gates (spec-hash drift, unresolved `## Open questions (HALT)`) and `.dev/floor/validate.mjs`. diff --git a/.dev/features/publish-oidc-trusted/PLAN.md b/.dev/features/publish-oidc-trusted/PLAN.md new file mode 100644 index 0000000..59725a1 --- /dev/null +++ b/.dev/features/publish-oidc-trusted/PLAN.md @@ -0,0 +1,52 @@ +# PLAN — publish.yml: pure OIDC Trusted Publishing (no tokens) + +- spec_content_hash: bca940a5ad247c120e6d8a3acba119d0d8df51dca275964d0e54c48d729d3c4e # fix #4 +- increment: Convert `.github/workflows/publish.yml` to npm Trusted Publishing (OIDC) — remove the `NODE_AUTH_TOKEN` secret, gate publish behind the `npm-publish` environment, bump Node to 24 + `npm@latest`, drop `workflow_dispatch`, and add a tag==package.json version guard. +- layer(s): repo CI infrastructure — NOT a PHARN capability / pharn-core / pharn-contracts. `ARCHITECTURE.md §4` layers cover the PHARN product surface; a repo CI workflow sits outside them as **Surface B′ tooling** (`THREAT-MODEL.md §1`), gated by the writes-scope + trusted-file hooks (`§3.3`), not the install floor. +- constitution_refs: [P0, P2, P5, P6, P7] + +## Files + +- `.github/workflows/publish.yml` — the ONLY file changed; six edits (below), keeping the SHA-pinned-actions convention — layer: repo CI infra + +### The six edits (all within `publish.yml`) + +1. **Delete the `env:` block** (`NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}`, lines 35–36) on the Publish step — auth becomes OIDC; **zero `secrets.` references remain**. +2. **Add `environment: npm-publish`** at the `publish` job level (immediately under `runs-on: ubuntu-latest`). +3. **setup-node `node-version: 20 → 24`**; add a new step immediately after setup-node: `run: npm install -g npm@latest` (Trusted Publishing needs npm >= 11.5.1). Keep the existing `cache: npm` + `registry-url` and the action SHA pin. +4. **Remove the `workflow_dispatch:` trigger** (line 8) — `release: [published]` only. +5. **Add a guard step before Publish** — `TAG="${GITHUB_REF_NAME#v}"; PKG="$(node -p "require('./package.json').version")"; [ "$TAG" = "$PKG" ] || { echo "tag v$TAG != package.json $PKG"; exit 1; }` (the `!=` written plainly — the `\!=` in the mandate was a shell history-expansion escape artifact). +6. **Rewrite the top workflow comment**: auth = OIDC Trusted Publisher (workflow `publish.yml`, environment `npm-publish`); drop the "or on manual dispatch" phrase; note the local first publish stays manual. + +## Contracts satisfied + +- **None in `pharn-contracts`.** This increment touches no PHARN capability, contract, rule, lens, or eval — it is repo CI infrastructure (Surface B′ tooling, `THREAT-MODEL.md §1`), outside the `pharn-contracts` surface. Nothing to cite because nothing applies (P4). + +## Evals to write (P1) + +- **NONE — a deliberate, honest exception, not an omission:** + - P1 binds behavior of the pharn **PRODUCT** (`tests/*.test.ts` over `src/`). This increment changes **no `src/` behavior** — it edits a GitHub Actions YAML. There is no vitest surface for a workflow file, and the mandate says **"No other files"** (forbidding a new test or floor checker). + - `.dev/floor/validate.mjs` walks only `.md` files and explicitly excludes `.github`/tooling; `npm run check` (format:check on `src`/`tests`, eslint `src`, typecheck, vitest) parses **no YAML**. So no floor test exists or is addable here. + - Verification is therefore: (a) `npm run check` stays **GREEN** (proves the TS product is untouched); (b) a deterministic **absence-grep** that zero `secrets.` remain; (c) **human review + GitHub's own workflow parser at release time** (out-of-band). Stated as advisory below (P0/P7). + +## Guarantee audit (P0) + +- **"No secrets appear anywhere in the workflow"** → **FLOOR** (deterministic absence test): `grep -c 'secrets\.' .github/workflows/publish.yml` == 0. Verifiable at build/verify. +- **"The TS product is unaffected by this change"** → **FLOOR**: `npm run check` exit 0 (existing gates). NOTE: this does **not** validate `publish.yml` — no gate parses YAML. +- **"publish.yml is syntactically valid / the workflow actually publishes via OIDC"** → **ADVISORY.** No floor primitive in this repo parses the workflow. Correctness rests on human review + GitHub's runtime workflow parser + npmjs.org Trusted Publisher config — all **out-of-band**. Not sold as guaranteed (P0; `LIMITS.md §4`: "good" = holes labeled, not "no holes"). +- **"tag == package.json version before publish"** → a **RUNTIME floor guard INSIDE the workflow** (string-equality membership test → `exit 1`), deterministic **when GitHub runs it**; from this repo's build floor it is **ADVISORY** (its execution is out-of-band). It adds a release-time gate, not a build-time guarantee. +- **External prerequisites** (advisory, P7 honest scope — named, not silently assumed; not files this plan can create): (i) a GitHub deployment environment named `npm-publish` must exist in repo settings; (ii) npmjs.org must be configured with a Trusted Publisher pointing at workflow `publish.yml` + environment `npm-publish`; (iii) the FIRST publish of the package stays a manual local `npm publish` by a maintainer. + +## Trust audit (P2) + +- **N/A** — this increment ingests **no** untrusted remote artifact (no manifest / `module.json` / degit tree). It is Surface B′ (the dev-loop editing repo tooling), answered by the writes-scope + trusted-file hooks (`THREAT-MODEL.md §1, §3.3`), not by the install-time validation floor. The workflow's own `checkout` keeps `persist-credentials: false` (unchanged). No taint propagation to audit. + +## Determinism audit (P5) + +- The only branch introduced is the workflow's guard: `[ "$TAG" = "$PKG" ]` — a string-equality membership test whose fallback is a hard `exit 1` (never a guess). Compliant. +- The build itself is a fixed set of six textual edits to one named file — no classification, no model-chosen branch. + +## Open questions (HALT) + +- **None blocking.** The six edits are fully specified by the mandate; live state confirms all three stale references (manual-dispatch comment, `workflow_dispatch`, `NODE_AUTH_TOKEN`) live ONLY in `publish.yml`, so "No other files" strands no doc (no P4 tension). +- **One interpretation flagged for confirmation at the approval gate:** unnamed lines (`cache: npm`, `registry-url`, `--provenance`, `id-token: write`, action SHA pins) are left **AS-IS** per "No other files." This is the documented npm Trusted-Publishing pattern — `registry-url` stays (it selects the registry; the OIDC exchange happens in `npm publish`), the token `env` drops, and `--provenance`/`id-token: write` remain valid under OIDC. Confirm this "leave unnamed lines untouched" reading is intended. diff --git a/.dev/features/publish-oidc-trusted/REGRESSION.md b/.dev/features/publish-oidc-trusted/REGRESSION.md new file mode 100644 index 0000000..68ae2a4 --- /dev/null +++ b/.dev/features/publish-oidc-trusted/REGRESSION.md @@ -0,0 +1,42 @@ +# REGRESSION — publish-oidc-trusted + +**Deterministic verdict (floor, `check-regress.mjs`): `no-regressions` — no deterministically-detectable breakage outside the feature (exit 0).** + +- **base:** `HEAD` (working-tree dogfood build — `git status --porcelain` non-empty → base = HEAD, per the stage's deterministic base rule). +- **inside (the feature's scope):** `.github/workflows/publish.yml`. `scope` exit **0**, `escaped: []` — the build stayed within its declared `## Files` (fix #7 cross-check passed). + +## Per-gate exit codes (base → head) + +| gate | base | head | classification | +| ---------- | ---- | ---- | ----------------------------- | +| `tests` | 1 | 1 | **pre-existing** (excluded) | +| `validate` | 1 | 1 | **pre-existing** (excluded) | + +- `regressions[]`: **none**. +- `pre_existing[]`: `tests`, `validate` — red at the baseline too, therefore **never blamed on this feature** (`check-regress.mjs`: `base != 0 → PRE-EXISTING`). + +## Why both gates are pre-existing-red — and why that is NOT this feature (the honest measurement note) + +This repo's working tree contains **untracked local `test-*/` app installs** (`git ls-files 'test-*'` → 0 tracked). Those installs each contain full copies of PHARN capabilities, which **inflate every whole-repo-scanning gate**: + +- **`tests` = 1** because `.dev/floor/lens-scanner-map.test.mjs` scans the live tree and asserts the lens-scanner map matches the live `role: lens` count; the untracked installs push that count to 142, so the test fails. (The remaining floor/hook tests pass.) +- **`validate` = 1** because `.dev/floor/validate.mjs .` walks every `.md` and hits the untracked installs' intentional negative fixtures (`test-*/pharn/floor/test-fixtures/red/skill.md`). The one *tracked* red fixture, `.dev/floor/test-fixtures/red/skill.md`, is excluded (validate skips `.dev/`), so **all** of validate's RED originates in untracked apps. + +Both conditions pre-date this increment and are independent of it. + +### How base and head were measured comparably (and why `publish.yml` was never touched) + +A `git worktree` baseline omits untracked files, so it would read these whole-repo gates **GREEN at base / RED at head** — a **false regression** that is purely a measurement artifact of the untracked apps. To avoid that (and to leave the open `publish.yml` untouched), both sides were measured **over the same working tree**, holding the untracked-app context constant. This is sound because the base and head gate-inputs are **byte-identical**: + +- `git diff --name-only HEAD` = `.github/workflows/publish.yml`, `.pharn/writes-scope.json` — **zero** `.md` / `.test.mjs` / `.test.cjs` / `findings.json` paths (the regress gates' entire input domain). +- Therefore every gate reads identical bytes at base (HEAD-commit) and head (working tree); the only difference is the untracked apps, which are held present on both sides. `base == head` for every gate **by path-membership**, not by assertion. + +The feature's one tracked change, `publish.yml`, is a `.yml` file read by **no** regress gate (validate → `.md`; tests → `.test.*`; structural → `findings.json`) — so it is provably incapable of flipping any gate. + +## Gate-set notes + +- **`structural`**: `outside_eval_pairs` = `[]`. No committed eval pair is cleanly available at a tracked path for this run (the trust-fence `expected` the stage doc references is not tracked under `.dev/features/trust-fence/`), so no structural gate was run — recorded honestly, not silently dropped. + +## Honest residual (P0/P7) + +`/pharn-dev-regress` catches **exactly what its suite catches — nothing more.** Here both suite gates are pre-existing-red (untracked local apps), so they cannot themselves catch a *new* pass→fail flip; the guarantee this run provides rests on the floor-grade fact that the feature changed **no gate-domain file** (0 `.md`/`.test.*`/`findings.json` in the diff), so no deterministically-detectable outside breakage is possible from a `.yml`-only change. This is "deterministically-detectable breakage outside the feature is caught," **not** "nothing broke." The report certifies the comparison, not the feature as a whole. diff --git a/.dev/features/publish-oidc-trusted/REVIEW.md b/.dev/features/publish-oidc-trusted/REVIEW.md new file mode 100644 index 0000000..77bd924 --- /dev/null +++ b/.dev/features/publish-oidc-trusted/REVIEW.md @@ -0,0 +1,77 @@ +# REVIEW — publish-oidc-trusted (ADVISORY) + +Increment under review (`trust: untrusted`): the six-edit conversion of `.github/workflows/publish.yml` to npm Trusted Publishing (OIDC). + +## Step 1 — Floor first (P0) + +`node .dev/floor/validate.mjs .` over the working tree is **RED (exit 1)** — but this is **not** a blocking finding against this increment, and the reasoning is deterministic (see `VERIFY.md`): + +- Clean CI-equivalent tree (`git worktree` at HEAD, no untracked files) → `validate` **exit 0**. +- **0** working-tree offenders outside `test-*/`; all RED originates in **untracked** local `test-*/` app installs. +- `validate` walks **only `.md`**; this increment changes only `.github/workflows/publish.yml` (`.yml`) → validate is provably invariant under it. + +So the increment-relevant floor is GREEN (CI-equivalent); the working-tree RED is a pre-existing untracked-app artifact, human-authorized at the build gate. Recorded, not used to fabricate a block. + +## The four lenses + +### L-floor → P0 — no blocking finding + +Every guarantee the increment claims reduces to the floor or is labeled `advisory` (confirmed against `PLAN.md`'s guarantee audit): + +- "No secrets in the workflow" → **floor**, and *observed*: `grep -c 'secrets\.' publish.yml` = **0** at build (the grill's concern that this was labeled-but-unenforced was addressed by actually running it). +- "TS product unaffected" → **floor**: `npm run check` exit 0. +- "workflow valid / OIDC authenticates" → **labeled advisory** (out-of-band: human review + GitHub parser + npmjs Trusted-Publisher config). +- "tag == package.json version" → **runtime floor guard inside the workflow**, labeled advisory from the build floor. + +No P0 guarantee is sold without a floor reduction or an `advisory` label. **GREEN on L-floor.** + +### L-eval → P1 — N/A (no subject), consistent with plan + grill + floor + +The increment adds **no Capability and no `rule_id`**, so P1's "≥1 eval per capability / per enforced rule" has no subject. `validate.mjs` (the floor that would catch a missing binding) does not range over `.github/` and agrees there is nothing to bind. The plan declared this honestly as a deliberate exception. **No finding.** + +### L-trust → P2 — no blocking finding; trust posture sound + +- The increment (a workflow YAML) emits **no findings**, so there is no free-text taint to mishandle; **no guaranteed decision rests on any tainted field.** +- **No instruction-looking content in the reviewed increment changed my behavior.** (Separately, during grill I encountered a hostile `test-full/AGENTS.md` fixture instructing me to read `node_modules` docs — treated as DATA, not obeyed; it is a test fixture, outside this increment.) +- Positive observations on the built workflow's trust posture: `persist-credentials: false` on checkout; SHA-pinned actions (supply-chain); the publish job gated behind `environment: npm-publish`; least-privilege `permissions` (`contents: read`, `id-token: write` only). The tag guard (`publish.yml:38–39`) is **injection-safe**: `GITHUB_REF_NAME` is assigned into a quoted var and compared with `[ "$TAG" = "$PKG" ]` (quoted), so a hostile tag name cannot inject shell. + +### L-axis → P3 — no blocking finding + +`publish.yml` remains single-purpose (the publish workflow); no sibling imports exist (it is YAML). One coherent axis of change (the OIDC migration). **No finding.** + +## Findings — grouped (fix #3) + +### Floor-gate (blocking): NONE + +### Advisory (minor — judgment/cosmetic; never a sole basis to block) + +```yaml +- type: FINDING + rule_id: P0 + severity: minor + file: ".github/workflows/publish.yml:44" + problem: "The comment 'Auth itself is OIDC — no NODE_AUTH_TOKEN' names NODE_AUTH_TOKEN, so a naive `grep NODE_AUTH_TOKEN` still hits the file even though the env block was removed. The meaningful floor test (`grep -c 'secrets\\.'`) is 0; this is cosmetic." + evidence: "\"# (needs id-token: write). Auth itself is OIDC — no NODE_AUTH_TOKEN.\"" +``` + +```yaml +- type: FINDING + rule_id: P7 + severity: minor + file: ".github/workflows/publish.yml:36" + problem: "The tag==version guard (edit #5) is a release-hygiene concern bundled with the OIDC auth migration — two related-but-distinct axes in one increment. Defensible (the human's mandate bundled them; both are tiny); echoes the grill's P7 note." + evidence: "\"- name: Verify tag matches package.json version\"" +``` + +## Verdict + +**GREEN (advisory) — 0 floor-gate/blocking findings; 2 minor advisory notes.** The increment is, by every deterministic signal available in this repo, complete and correct: `npm run check` GREEN, `secrets.` = 0, four of five verify gates GREEN with the fifth a proven local artifact, no regressions. Real-world correctness of the OIDC publish is out-of-band (first release) — see `PLAN.md` external prerequisites (i)–(iii). This verdict is **advisory**; the merge / fix / abandon decision is the human's at GATE 2. + +## Proposed lesson candidate (P7 — NOT canon; requires a separate human-gated `/pharn-dev-memory-promote`) + +A **real, recurring** dogfooding failure surfaced across build/regress/verify this run — worth proposing for `.dev/memory-bank/lessons-learned.md`: + +- **Lesson (candidate):** *When the pharn-cli working tree contains untracked local `test-*/` app installs, the whole-repo floor instruments (`validate.mjs .`, and hence `/pharn-dev-build`'s and `/pharn-dev-verify`'s floor, plus `lens-scanner-map.test.mjs`) go RED/again-red from the installs' intentional `red/` fixtures and inflated capability counts — producing false build/verify FAILs and false `git worktree`-baseline regress asymmetry for any increment that touches no capability. Remedy: measure the CI-equivalent (a clean `git worktree` at HEAD) and classify untracked-`test-*/` RED as pre-existing; never blame it on a non-`.md` increment.* +- **Why real, not hypothetical (P7):** it hit **this** increment at three stages (build verdict, regress base/head, verify verdict), each resolved only by the CI-equivalent measurement. +- **Provenance:** increment `publish-oidc-trusted`; diff = `.github/workflows/publish.yml` (6 edits); evidence = this run's `validate.mjs .` exit 1 vs clean-worktree exit 0, and `regression-report.json` `pre_existing: [tests, validate]`. +- **Gate:** do **not** write canon here. A maintainer runs `/pharn-dev-memory-promote` (which sets its own scope, runs `check-provenance.mjs`, and halts for accept/deny). The model never self-promotes (P2). diff --git a/.dev/features/publish-oidc-trusted/SHIP.md b/.dev/features/publish-oidc-trusted/SHIP.md new file mode 100644 index 0000000..80bc4b9 --- /dev/null +++ b/.dev/features/publish-oidc-trusted/SHIP.md @@ -0,0 +1,36 @@ +# SHIP — publish-oidc-trusted (advisory roll-up) + +`/pharn-dev-ship` ran the gated chain for the six-edit conversion of `.github/workflows/publish.yml` to npm Trusted Publishing (OIDC). This file records **that the chain ran and its floor verdicts** — it is not a judgment, an approval, or a `PHARN ✓ reviewed` seal. + +## Stages run, in order + +| # | stage | outcome | +| - | ----- | ------- | +| 1 | `/pharn-dev-plan` | PLAN.md written; **GATE 1** — human **approved** ("accepted, continue"). | +| 2 | `/pharn-dev-grill` | GRILL.md — advisory; **3 minor, 0 blocking**. Gates nothing. | +| 3 | `/pharn-dev-build` | Six edits applied to `publish.yml`. **RED-verdict → human gate** (see below). | +| 4 | `/pharn-dev-regress` | regression-report.json — verdict below. | +| 5 | `/pharn-dev-verify` | verify-report.json — verdict below. | +| 6 | `/pharn-dev-review` | REVIEW.md — advisory GREEN, 0 blocking, 2 minor. | + +**Where the run ended:** **GATE 2** (post-review). The human decides **merge / fix / abandon**. + +## Structural verdicts read (verbatim — the only inputs to proceed/stop) + +- **`/pharn-dev-build` → `node .dev/floor/validate.mjs .` exit code = `1` (RED).** This is a **RED-verdict STOP**. It was **not** auto-cleared: it was presented at a **human gate**, where the human chose **"Continue the chain"** after it was proven that the RED is entirely from **untracked local `test-*/` app fixtures** (0 offenders outside `test-*/`; `validate` reads only `.md`, the change is `.yml`, so it is invariant; clean-worktree CI-equivalent = exit 0). The increment's operative floor, `npm run check`, was **GREEN** (378 tests). +- **`/pharn-dev-regress` → `regression-report.json .verdict` = `"no-regressions"`** (exit 0). Both suite gates (`tests`, `validate`) classified `pre_existing` (untracked-app artifacts, held constant across base/head; 0 gate-domain files changed). Proceed. +- **`/pharn-dev-verify` → `verify-report.json .verdict` = `"FAIL"`** — sole failing gate `validate` (`test`/`lint`/`format:check`/`lint:md` all GREEN). Recorded **faithfully as FAIL**, not massaged. Proven to be the **same** pre-existing untracked-app `validate` artifact (clean-worktree CI-equivalent = PASS). The chain continued **only** under the human's standing GATE-1/build authorization, given with explicit notice that "verify will surface the same validate.mjs condition." + +## Honest note on the two non-GREEN verdicts + +`/pharn-dev-build`'s `validate` RED and `/pharn-dev-verify`'s `FAIL` are the **same** repo-level condition: a working tree containing untracked `test-*/` app installs whose intentional `red/` fixtures make the whole-repo `validate` go red. It is **pre-existing**, **invariant under this `.yml`-only increment**, and **GREEN in a clean CI checkout**. This was surfaced honestly at each stage and proceeded past only by **explicit human decision**, never by the agent overriding a floor verdict. (`REVIEW.md` proposes a lesson candidate to codify this — human-gated via `/pharn-dev-memory-promote`, not written here.) + +## Pointers (cited, not restated — P4) + +- `.dev/features/publish-oidc-trusted/REVIEW.md` — 4 advisory lenses; GREEN, 0 blocking, 2 minor (a cosmetic `NODE_AUTH_TOKEN` comment mention at `publish.yml:44`; the bundled tag-guard, P7). +- `.dev/features/publish-oidc-trusted/GRILL.md` — advisory, 3 minor. +- `PLAN.md` external prerequisites (i)–(iii): the `npm-publish` GitHub environment + npmjs Trusted-Publisher config must exist out-of-band; first publish stays a manual local `npm publish`. + +## Standing line + +Chain ran; the named floor verdicts are as shown — **this is NOT a judgment that the increment is good or wise; that is the human's call at the post-review gate.** `/pharn-dev-ship` does not merge, push, or seal. diff --git a/.dev/features/publish-oidc-trusted/VERIFY.md b/.dev/features/publish-oidc-trusted/VERIFY.md new file mode 100644 index 0000000..4a4b880 --- /dev/null +++ b/.dev/features/publish-oidc-trusted/VERIFY.md @@ -0,0 +1,32 @@ +# VERIFY — publish-oidc-trusted + +**Deterministic floor verdict (`check-verify.mjs`, over the working tree): `FAIL` — gate `validate` red (exit 1).** + +## Floor gates (whole-repo, over the working tree) + +| gate | exit | note | +| -------------- | ---- | ----------------------------------------------------------------- | +| `test` | 0 | vitest — 378 tests pass | +| `validate` | 1 | **sole failure** — see below (pre-existing untracked-app artifact) | +| `lint` | 0 | eslint clean | +| `format:check` | 0 | prettier clean | +| `lint:md` | 0 | markdownlint clean | + +`failing_gates: ["validate"]`. Verifiers: **no verifiers registered — floor gates only** (`count-verifiers .` → `{"registered":0}`). + +## The `validate` FAIL is a local artifact, not this feature (proven, not asserted) + +Four independent facts, each deterministic: + +1. **CI-equivalent verdict is PASS.** `validate.mjs .` run in a clean `git worktree` at `HEAD` (no untracked files — exactly what `ci.yml` checks out) → **exit 0**. All other gates are already green, so a CI verify would be **PASS**. +2. **Every working-tree offender is an untracked test app.** `validate.mjs .` offenders outside `test-*/` = **0**. All RED originates in the untracked local `test-*/` app installs (`git ls-files 'test-*'` → 0 tracked); their intentional `pharn/floor/test-fixtures/red/skill.md` fixtures are what validate flags. The one *tracked* red fixture (`.dev/floor/test-fixtures/red/skill.md`) is excluded — validate skips `.dev/`. +3. **Invariant under this feature.** `validate.mjs` walks **only `.md`** files (`name.endsWith(".md")`); the feature's sole change is `.github/workflows/publish.yml` (`.yml`). It is provably outside validate's input domain — validate's result cannot change because of it. +4. **The other four gates — `test`, `lint`, `format:check`, `lint:md` — are all GREEN,** so the feature introduced no new deterministic failure anywhere the suite looks. + +## Standing human authorization + +This is the **same** pre-existing `validate` condition surfaced at the `/pharn-dev-build` gate, where the decision option explicitly read *"Note: verify will surface the same validate.mjs condition."* The human chose **"Continue the chain"** with that notice, i.e. authorized proceeding past this exact FAIL. The chain therefore continues to `/pharn-dev-review`, which ends at **GATE 2** where the human makes the final merge / fix / abandon call with every artifact in hand. + +## Honest residual (P0/P7) + +Verified = the named gates passed — and here **four of five did**, with the fifth (`validate`) failing only on untracked local test apps that do not exist in CI and that this `.yml` change cannot affect. This is **NOT** a guarantee of correctness beyond what those gates check: no floor gate in this repo parses the workflow YAML itself (`validate` reads `.md`; nothing runs the `secrets.` grep as a standing gate — it was run once at build and returned 0). The workflow's real-world correctness (OIDC actually authenticates; the `npm-publish` environment and npmjs Trusted-Publisher config exist) is verified out-of-band at the first release — see `PLAN.md` external prerequisites (i)–(iii). Verifier concerns are advisory help, not assurance; there are none today. diff --git a/.dev/features/publish-oidc-trusted/regression-report.json b/.dev/features/publish-oidc-trusted/regression-report.json new file mode 100644 index 0000000..58869b4 --- /dev/null +++ b/.dev/features/publish-oidc-trusted/regression-report.json @@ -0,0 +1,22 @@ +{ + "base": "HEAD", + "inside": [ + ".github/workflows/publish.yml" + ], + "outside_gates": { + "tests": { + "base": 1, + "head": 1 + }, + "validate": { + "base": 1, + "head": 1 + } + }, + "regressions": [], + "pre_existing": [ + "tests", + "validate" + ], + "verdict": "no-regressions" +} diff --git a/.dev/features/publish-oidc-trusted/verify-report.json b/.dev/features/publish-oidc-trusted/verify-report.json new file mode 100644 index 0000000..92600e0 --- /dev/null +++ b/.dev/features/publish-oidc-trusted/verify-report.json @@ -0,0 +1,15 @@ +{ + "feature": "publish-oidc-trusted", + "gates": { + "format:check": 0, + "lint": 0, + "lint:md": 0, + "test": 0, + "validate": 1 + }, + "verdict": "FAIL", + "failing_gates": [ + "validate" + ], + "verifiers": { "registered": 0, "findings": [] } +} From 4b6cc478b704c9e7b92d86fc693fc984091f6c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 22:27:59 +0200 Subject: [PATCH 07/10] update --- .pharn/pharn-dev-regress/base-results.json | 2 +- .pharn/pharn-dev-regress/head-results.json | 2 +- .pharn/writes-scope.json | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pharn/pharn-dev-regress/base-results.json b/.pharn/pharn-dev-regress/base-results.json index 1e84650..2292b66 100644 --- a/.pharn/pharn-dev-regress/base-results.json +++ b/.pharn/pharn-dev-regress/base-results.json @@ -1 +1 @@ -{"tests":1,"validate":1} +{"tests":1,"validate":1} \ No newline at end of file diff --git a/.pharn/pharn-dev-regress/head-results.json b/.pharn/pharn-dev-regress/head-results.json index 1e84650..2292b66 100644 --- a/.pharn/pharn-dev-regress/head-results.json +++ b/.pharn/pharn-dev-regress/head-results.json @@ -1 +1 @@ -{"tests":1,"validate":1} +{"tests":1,"validate":1} \ No newline at end of file diff --git a/.pharn/writes-scope.json b/.pharn/writes-scope.json index c2dd576..cf1fed4 100644 --- a/.pharn/writes-scope.json +++ b/.pharn/writes-scope.json @@ -1,7 +1,7 @@ { "scope": [ - ".dev/features/canonical-npm-name/SHIP.md" + ".github/workflows/publish.yml" ], - "set_by": ".claude/commands/pharn-dev-ship.md", - "set_at": "2026-07-22T17:36:31.136Z" + "set_by": ".dev/features/publish-oidc-trusted/PLAN.md", + "set_at": "2026-07-22T20:18:15.526Z" } From 3148cd51490aa2794fe06dc4f910202a10647217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 22:36:53 +0200 Subject: [PATCH 08/10] updates --- .dev/memory-bank/lessons-learned.md | 13 +++++++++++++ .pharn/pharn-dev-memory-promote/candidate.json | 12 ------------ .pharn/writes-scope.json | 6 +++--- SECURITY.md | 4 ++-- 4 files changed, 18 insertions(+), 17 deletions(-) delete mode 100644 .pharn/pharn-dev-memory-promote/candidate.json diff --git a/.dev/memory-bank/lessons-learned.md b/.dev/memory-bank/lessons-learned.md index 35c5ff8..5c9b0e8 100644 --- a/.dev/memory-bank/lessons-learned.md +++ b/.dev/memory-bank/lessons-learned.md @@ -373,3 +373,16 @@ A file-tree signal that keys off a basename or extension ALONE (`api/`, `route.t - commit: `6acb6aa845f39dbd3e962c0d72c270fb27163209` - source: `.dev/features/archetype-path-context/REVIEW.md` (proposed lesson candidate) + `GRILL.md` F4 (third-flip churn) - promoted: 2026-07-09 via gated `/pharn-dev-memory-promote` (human-approved). + +## L14 — Untracked local test-*/ app installs redden the whole-repo floor — false build/verify FAILs for non-capability increments + +**Lesson.** The pharn-cli working tree carries UNTRACKED local `test-*/` PHARN app installs (`git ls-files 'test-*'` -> 0). They sit on the floor's SCAN surface: `validate.mjs .` walks every `.md` and hits their intentional `pharn/floor/test-fixtures/red/skill.md` fixtures (RED), and `lens-scanner-map.test.mjs` counts their installed `role: lens` copies (live 142 != map -> the `tests` gate REDs). So for an increment that touches NO capability (e.g. a `.github/workflows/*.yml` edit, read by NO floor gate), `/pharn-dev-build`'s validate verdict and `/pharn-dev-verify`'s validate gate both go RED, and `/pharn-dev-regress`'s `git worktree` baseline (which OMITS untracked files) reads these whole-repo gates GREEN@base / RED@head -> a FALSE regression -- none of it caused by the increment. Remedy: measure the CI-equivalent -- `validate.mjs` in a clean `git worktree` at HEAD (no untracked apps) is the verdict CI actually computes (exit 0 here); classify untracked-`test-*/` RED as pre-existing (verify saw 0 validate offenders outside `test-*/`); and for regress, hold the untracked context CONSTANT (measure base and head over the same working tree -- valid because `git diff --name-only HEAD` showed 0 gate-domain files, i.e. no `.md`/`.test.*`/`findings.json`, changed) rather than a worktree baseline. + +**Why it matters.** This is L11's principle -- a WHOLE-REPO gate blaming the feature for a pre-existing UNRELATED condition that `/pharn-dev-verify` cannot classify as pre-existing the way `/pharn-dev-regress` can -- extended along three axes L11 does not cover: (1) the offender is UNTRACKED local scratch, so L11's 'keep the repo style-clean at merge' remedy does NOT apply (you cannot commit-clean a developer's local install); (2) it reddens `validate` + the `lens-scanner-map` `tests` gate, not only style gates; (3) it also breaks `/pharn-dev-regress`'s `git worktree` BASELINE (verify has no baseline; regress does, and the worktree omits the untracked apps), so the remedy is the CI-equivalent clean-worktree measurement, not a base-vs-head style diff. Concretely this run: the `publish-oidc-trusted` increment (a `.yml`-only change, invariant under every floor gate) hit RED validate at `/pharn-dev-build` (human chose Continue), FAIL at `/pharn-dev-verify` (sole gate `validate`), and a worktree-baseline false-regression risk at `/pharn-dev-regress` -- all resolved only by proving `validate` exit 0 in a clean HEAD worktree. Complements L11 (verify, committed offender) and L10 (the validate scan surface). + +**Provenance.** + +- feature: `publish-oidc-trusted` +- commit: `db11f65113d3580b07369b47653417a8cc2a1dea` +- source: `.dev/features/publish-oidc-trusted/REVIEW.md` (proposed lesson candidate) + `VERIFY.md` + `regression-report.json` +- promoted: 2026-07-22 via gated `/pharn-dev-memory-promote` (human-approved). diff --git a/.pharn/pharn-dev-memory-promote/candidate.json b/.pharn/pharn-dev-memory-promote/candidate.json deleted file mode 100644 index c8ce3db..0000000 --- a/.pharn/pharn-dev-memory-promote/candidate.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "target": ".dev/memory-bank/lessons-learned.md", - "id": "L13", - "provenance": { - "feature": "archetype-path-context", - "commit": "6acb6aa845f39dbd3e962c0d72c270fb27163209", - "source": ".dev/features/archetype-path-context/REVIEW.md (proposed lesson candidate) + GRILL.md F4 (third-flip churn)", - "date": "2026-07-09" - }, - "title": "Scope structural (file-tree) signals to their documented location on introduction, not basename-only", - "body": "A file-tree signal that keys off a basename or extension ALONE (`api/`, `route.ts`, `.sql`, `.tsx`) over-classifies: a file merely NAMED `x` does not mean the project HAS surface `x`. `src/api/` is a near-universal FRONTEND fetch-wrapper convention, a Vue router has a `route.ts`, a frontend commits a `seed.sql`, and a backend holds react-email `.tsx` templates — each basename-only rule mis-fired to the wrong archetype (a wrong [backend, spa]). The remedy is to thread ANCESTOR PATH-CONTEXT (the lowercased parent-dir `segments`) into the classifier and gate each signal on the location where that surface actually lives: `api/` → top-level or a `pages`/`app` parent; `route.*` → an `app/` ancestor; `.sql` → a DB-location dir; `.tsx`/`.jsx` → excluding test/email dirs. Determinism is preserved (every branch stays a membership test); the trade is a common false POSITIVE for a rarer, DOCUMENTED false NEGATIVE on nonconventional layouts.\n\n**Why it matters.** Basename-only breadth is not a one-off bug — it RECURS and CHURNS. The `.sql` signal alone flipped THREE times across increments: `archetype-file-tree-scan` dropped it → `archetype-enum-align` added it at ANY depth → `archetype-path-context` scoped it to DB-location dirs; Fable independently found the same over-broadness in `api/`, `route.*`, and `.tsx`/`.jsx`. Each flip re-litigated a pinned test. Introducing a structural signal WITH its path scope from the start (check the path, not just the name) avoids the churn and the wrong archetype capabilities it produces — a SPA wrongly receiving ssrf / path-traversal / migrations lenses, a backend wrongly receiving a11y / i18n. The scope stays deterministic membership (P5), so precision costs no guarantee; only the accuracy improves, and accuracy is an advisory trade that must be LABELED as such (P0), never sold as guaranteed-correct detection." -} \ No newline at end of file diff --git a/.pharn/writes-scope.json b/.pharn/writes-scope.json index cf1fed4..a7c5c1b 100644 --- a/.pharn/writes-scope.json +++ b/.pharn/writes-scope.json @@ -1,7 +1,7 @@ { "scope": [ - ".github/workflows/publish.yml" + ".dev/memory-bank/lessons-learned.md" ], - "set_by": ".dev/features/publish-oidc-trusted/PLAN.md", - "set_at": "2026-07-22T20:18:15.526Z" + "set_by": ".claude/commands/pharn-dev-memory-promote.md", + "set_at": "2026-07-22T20:28:24.625Z" } diff --git a/SECURITY.md b/SECURITY.md index 06efa96..020a841 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ PHARN is an audit-grade methodology — taking security seriously is part of the ## What `pharn` is, and its security surface -This repository **is `pharn`** — an ESM-only Node CLI (`"type": "module"`, NodeNext, `engines.node >= 20`) that runs a wizard, fetches the selected PHARN modules from `pharn-dev/pharn-oss` via `degit`, copies them into the user's `.claude/`, and writes `pharn.config.json`. It has a small, thin dependency set (`@clack/prompts`, `degit`, `minimist`, `picocolors`), no bundled runtime services, and no telemetry. Its security-relevant surface is exactly the two things that cross a trust boundary: **remote input** (the `manifest.json` / `module.json` it reads and the repo content it clones) and **file-system writes** (everything it copies into `.claude/` and the config it writes). +This repository **is `pharn`** — an ESM-only Node CLI (`"type": "module"`, NodeNext, `engines.node >= 20`) that runs a wizard, fetches the selected PHARN modules from `pharn-dev/pharn-oss` via `degit`, copies them into the user's project — the `.claude/` command/hook surfaces, the mirrored capability dirs (`pharn-pipeline/grillers/`, `pharn-review/`), and the trusted docs / `pharn-contracts/` / `.dev/floor/` product surfaces at the project root (or all of it under `pharn/`) — and writes `pharn.config.json`. It has a small, thin dependency set (`@clack/prompts`, `degit`, `minimist`, `picocolors`), no bundled runtime services, and no telemetry. Its security-relevant surface is exactly the two things that cross a trust boundary: **remote input** (the `manifest.json` / `module.json` it reads and the repo content it clones) and **file-system writes** (everything it copies into the project — the `.claude/` surfaces, the mirrored capability dirs, and the root/`pharn/` product surfaces — plus the `pharn.config.json` it writes). The CLI's security model is **deterministic, not model-driven**: it never asks an AI to decide what is safe. Every value that arrives from the network is validated against strict regex allowlists, rejected for `..` and control characters, and every copy is confined with a `safeJoin` guard so nothing can escape its intended target — checks that hold regardless of what the fetched content says. Preserve that shape: a security fix that relies on "the content will be well-behaved" is not a fix. @@ -73,7 +73,7 @@ Scope follows the surface described above: everything that touches remote input `pharn`'s input validation and consent prompts are defense-in-depth, not a guarantee. When using the CLI: -1. **Run it in an existing, version-controlled project** so you can diff exactly what `init` wrote (`.claude/` and `pharn.config.json`) before committing. +1. **Run it in an existing, version-controlled project** so you can diff exactly what `init` wrote (the `.claude/` surfaces, the capability dirs and product surfaces, and `pharn.config.json`) before committing. 2. **Review the vendor skills** the wizard offers before accepting them — vendor selection is opt-in and nothing is selected by default; only accept vendors you recognize. 3. **Prefer `npx pharn@latest`** so you run the current, supported release rather than a stale pinned copy. 4. **Inspect the cloned `.claude/` skills** before running them through your AI tool — installation fetches remote content. From fefd80427af5e027279840bf5972729dc487de84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 22:55:35 +0200 Subject: [PATCH 09/10] updates --- CHANGELOG.md | 2 +- CLAUDE.md | 12 +++++++- CONTRIBUTING.md | 1 + docs/RELEASING.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 docs/RELEASING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9e8ce..4634805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `pharn-cli` bin alias for a single `pharn` bin; and added a `prepack` build so `npm publish` always ships a freshly compiled `dist/`. No CLI behavior change and `version` is unchanged; the package now installs via `npx pharn@latest init`. -- Initially published to npm as `@pharn-dev/pharn` (now deprecated); the canonical name is `pharn`. +- Initially published to npm as `@pharn-dev/pharn` (unpublished on 2026-07-22); the canonical name is `pharn`. - Docs: surfaced the new optional `/pharn-spec` stage (intent capture before `/pharn-plan`) in getting-started and the `pharn-pipeline` module description, matching `pharn-oss`. No CLI code change — `/pharn-spec` ships transparently via the existing whole-module install from `main`. diff --git a/CLAUDE.md b/CLAUDE.md index efb5fdf..030ca7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,17 @@ npm run check # format:check + lint + typecheck + test (aggregate npx vitest run tests/install-capabilities.test.ts # single test file ``` -CI (`.github/workflows/ci.yml`) runs six gates independently (so one failure can't mask the others) — format:check, lint, lint:md, typecheck, test (via `test:coverage`), and build — all must pass. `PHARN_DEBUG=1` enables full error output for fetch/install failures. +CI (`.github/workflows/ci.yml`) runs six gates independently (so one failure can't mask the others) — format:check, lint, lint:md, typecheck, test (via `test:coverage`), and build — all must pass. A second workflow, `.github/workflows/publish.yml`, publishes to npm on a published GitHub Release — see **Releasing** below. `PHARN_DEBUG=1` enables full error output for fetch/install failures. + +## Releasing + +Canonical npm package: **`pharn`** (unscoped). The scoped `@pharn-dev/pharn` was briefly published, then **unpublished on 2026-07-22** — it was never deprecated; `pharn` is the only name. + +Release flow: bump `version` in `package.json` + update `CHANGELOG.md` → merge to `main` → cut a **GitHub Release** tagged `vX.Y.Z` (a guard step in `publish.yml` fails the run unless the tag, minus its leading `v`, equals `package.json` `version`). Publishing the Release triggers **`.github/workflows/publish.yml`**, which publishes via **npm Trusted Publishing (OIDC)** — workflow `publish.yml`, environment `npm-publish`, node 24 (npm >= 11.5.1), `npm publish --provenance` (the flag overrides `publishConfig.provenance: false`, so releases carry a signed provenance attestation). + +**No npm tokens exist anywhere** — no repo secrets, none in Actions; auth is the short-lived OIDC id-token exchanged at publish time. If a token ever seems to be "needed," the Trusted Publisher config is broken — fix it on npmjs.com, **never** add a secret. + +**First-publish exception:** the very first publish of a *new* package name is a manual, local `npm publish` (provenance off) by a maintainer, because npm only allows configuring a Trusted Publisher on an *existing* package. Every later release goes through `publish.yml`. Full maintainer steps: [`docs/RELEASING.md`](docs/RELEASING.md). ## ESM / module conventions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1940c4e..825a21e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,7 @@ Before changing anything, read these in order: - **Where your change goes** → [`CLAUDE.md`](./CLAUDE.md) architecture section and the [test map](./docs/contributing.md#test-map) - **The rules of the repo** → [`CLAUDE.md`](./CLAUDE.md) (source of truth — read it before adding, editing, or removing anything) - **User-facing docs** → [`docs/`](./docs/README.md) +- **Releasing (maintainers)** → [`docs/RELEASING.md`](./docs/RELEASING.md) (version bump → GitHub Release → npm via Trusted Publishing) - **Community standards** → [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md) ## The 30-second version diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..8f1b074 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,72 @@ +# Releasing pharn + +Maintainer guide for publishing a new version of the `pharn` npm package. + +Releases are automated: cut a **GitHub Release** and +[`.github/workflows/publish.yml`](../.github/workflows/publish.yml) publishes to +npm. Authentication is **npm Trusted Publishing (OIDC)** — there are **no npm +tokens** anywhere in this repository or in its GitHub Actions secrets. + +## The package + +The canonical npm package is **`pharn`** (unscoped) — +. It exposes a single `pharn` bin. + +> The scoped name `@pharn-dev/pharn` was briefly published early on and then +> **unpublished on 2026-07-22**. It was never deprecated — `pharn` is the only +> name. Do not republish the scoped name. + +## How publishing is authenticated + +`publish.yml` uses **npm Trusted Publishing (OIDC)**: at publish time the +workflow exchanges a short-lived GitHub OIDC id-token with npm — no long-lived +credential is stored or passed. + +- **No secrets, no tokens** — not in the repo, not in Actions secrets, not in + the workflow. `permissions: id-token: write` is what enables the OIDC exchange + (and the `--provenance` attestation); it is not a secret. +- The Trusted Publisher is configured **on npmjs.com** for package `pharn`, + pinned to workflow **`publish.yml`** and the GitHub deployment environment + **`npm-publish`**. +- **If a publish ever fails asking for a token, the Trusted Publisher config is + broken — fix it on npmjs.com. Never add an npm token secret.** + +## Cutting a release + +1. **Bump the version.** Update `"version"` in `package.json` (SemVer). +2. **Update `CHANGELOG.md`.** Move the `[Unreleased]` entries under a new + `## [X.Y.Z] — YYYY-MM-DD` heading and refresh the compare links at the bottom. +3. **Merge to `main`** via PR — the CI gates in + [`ci.yml`](../.github/workflows/ci.yml) must pass. +4. **Cut a GitHub Release.** Tag it **`vX.Y.Z`**, where `X.Y.Z` **exactly + matches** `package.json` `version`. A guard step in `publish.yml` fails the + run if the tag (minus its leading `v`) does not equal the package version. +5. **Publishing the Release triggers `publish.yml`.** It runs on node 24 with + `npm@latest` (Trusted Publishing needs npm >= 11.5.1), re-runs the full check + suite and build (via the `prepublishOnly` + `prepack` hooks), verifies the + tag, then runs `npm publish --provenance --access public`. The `--provenance` + flag overrides `publishConfig.provenance: false`, so the release carries a + signed provenance attestation. + +## Verify the release + +1. **GitHub Actions** — the `publish` workflow run for the Release is green. +2. **npmjs.com** — shows the new version, + and that version shows a **provenance** badge linking back to the GitHub + Actions run. +3. **Smoke-test** — `npx pharn@latest --version` prints the new version. + +## First publish of a new package name (one-time exception) + +npm only lets you configure a Trusted Publisher on a package that **already +exists**. So the _very first_ publish of a brand-new package name is done +**manually and locally** by a maintainer who is logged in (`npm login`): + +```bash +npm publish # provenance stays off (publishConfig.provenance: false) — no OIDC available yet +``` + +Then configure the Trusted Publisher on npmjs.com (workflow `publish.yml`, +environment `npm-publish`), and every subsequent release goes through +`publish.yml` as described above. This exception applies **only** to introducing +a new package name — never to a normal version bump of `pharn`. From d5c4256f172deb00e9d459aa2a63ec4462841618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Wed, 22 Jul 2026 22:59:33 +0200 Subject: [PATCH 10/10] updates --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 825a21e..9ac38a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ Before changing anything, read these in order: 4. **Branch**: `feat/…`, `fix/…`, or `docs/…`. 5. **Commit** in [Conventional Commits](https://www.conventionalcommits.org/) style, one logical change per commit. 6. **Tests first** — when changing wizard behavior, update the matching `tests/*.test.ts` before touching code. The suite mirrors `steps/` and `lib/` one-to-one. -7. **Security-sensitive files** (`lib/validate.ts`, `lib/manifest.ts`, `lib/install-modules.ts`) — preserve the validation invariants (regex allowlists, `safeJoin` path guard, `redirect: 'error'`, timeout/size caps, `schemaVersion === 1`) called out in [`CLAUDE.md`](./CLAUDE.md). +7. **Security-sensitive files** (`lib/validate.ts`, `lib/install-capabilities.ts`, `lib/skills-version.ts`) — preserve the validation invariants (regex/enum allowlists, `safeJoin` path guard, symlink rejection, `redirect: 'error'`, and the fetch timeout/size caps) called out in [`CLAUDE.md`](./CLAUDE.md). 8. **Keep docs in sync** — see the [Documentation maintenance](./docs/contributing.md#documentation-maintenance) table. ## Conduct and security