Commit Graph
7 Commits
Author SHA1 Message Date
taodengandClaude Opus 4.7 a718d22900 ci: D37 — release.yml phase_rolling_mode gate (issue #17)
Round-6 F14: release.yml verified package.json/tag version match and
extracted the matching CHANGELOG section, but did not verify that
"## Unreleased" had been promoted to "## v<version>" before the tag
push. If someone tagged v0.1.0-bootstrap today, release.yml would
extract the stale ## v0.1.0-bootstrap section, ignoring all D-day
work folded under Unreleased — the exact failure mode documented in
MEMORY.md 2026-04-21 ("release.yml would publish stale notes that
ignored Unreleased amendments").

Option A applied: CI gate makes the policy enforceable. Manual
checklist (Option B) was rejected because it relies on human memory,
which is what produced the original failure.

New step "Enforce phase_rolling_mode (Unreleased must be promoted)"
runs after version-match check and before CHANGELOG extraction:

1. Extracts content between "## Unreleased" heading and the next
   "## " heading via awk.
2. Strips blank lines and parenthetical-sentinel lines via sed
   (acceptable forms: "(empty — Phase N entries land here once
   Phase N opens)", "(another sentinel)", multi-sentinel blocks).
3. If any non-trivial content remains, exits with ::error::
   instructing the maintainer to promote Unreleased → ## v<version>
   per CLAUDE.md release_kit.phase_rolling_mode.

Locally dry-run against 4 cases:
- Current CHANGELOG.md (sentinel-only Unreleased) → PASS
- Synthetic CHANGELOG with bullet-list under Unreleased → gate FIRES,
  reports offending lines indented for readability
- Synthetic CHANGELOG with no Unreleased section → PASS
- Synthetic CHANGELOG with multiple parenthetical sentinels and
  intervening blank lines → PASS

Authority:
- CLAUDE.md release_kit.phase_rolling_mode (D33 F11 added the policy;
  this gate enforces it)
- MEMORY.md 2026-04-21 OCP cross-machine sync entry (documents the
  same class of failure as a prior precedent)
- Round-6 cold-audit Finding 14

Gate is purely additive — adds a check, does not modify existing
release behavior. Fires only on tag push to v*.*.* — does not affect
normal push/PR CI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 20:41:45 +10:00
taodengandClaude Opus 4.7 2600185edb fix+test: D35 — pre-Phase-2 batch #1 (issues #4 #9 #10 #11 #12)
First batch of pre-Phase-2 cleanup work. 5 GitHub issues closed in
one cohesive commit covering streaming-path correctness, IR
validator hardening, and CI path-trigger hygiene.

Changes (4 files, +302 / -5):

**Code fixes**

1. **#9 — Streaming empty-then-clean-exit headers** (server.mjs)

   Pre-D35: when a provider's streaming spawn finished cleanly with
   zero chunks (e.g. spec-degenerate stop with no content), the
   response went out the SSE_DONE / res.end path without ever calling
   writeHead. Result: client saw stream open + close with no headers,
   no status code path applied. Now: zero-chunk branch guards
   `!res.headersSent` and emits Content-Type + Cache-Control +
   Connection + X-Accel-Buffering + all 5 X-OLP-* headers via
   olpHeaders (provider attempted, cache miss) before writing the
   terminator. Zero-chunk path correctly does NOT cache (cache write
   remains gated on irChunk.type === 'stop').

2. **#10 — Streaming post-first-chunk error truncation marker**
   (server.mjs, two sibling sites)

   Pre-D35: if a provider yielded an error AFTER first content chunk
   was emitted, the SSE stream was abandoned with raw socket close.
   Client SDKs that wait for finish_reason hung. Now:

   - Catch-block firstChunkEmitted=true path: emit synthetic
     `{type:'stop', finish_reason:'length'}` via irChunkToOpenAISSE,
     write SSE_DONE, end. Per ADR 0004 § Fallback safety: post-first-
     chunk truncation surfaces as `length` finish, not a hang.

   - Sibling fix in error-chunk path (provider yields `type:'error'`
     chunk AFTER first content chunk): same recovery (marker + DONE +
     end). Scope-creep acknowledged but identical semantic; clean to
     fix together. Comments cross-reference D26 F19 and D35 #10.

3. **#11 — validateIRRequest irVersion strict check** (lib/ir/types.mjs)

   ADR 0003 IR contract pins irVersion to '1.0'. Validator pre-D35
   accepted ANY value (including no value, undefined, '2.0',
   numeric 1.0). Now: `obj.irVersion !== undefined && obj.irVersion
   !== '1.0'` → rejection. Strict string match. `undefined` still
   accepted (pre-D35 IRs without the field remain valid — back-compat
   with sites that haven't yet been migrated to emit it). Error
   message uses JSON.stringify for safe rendering.

4. **#12 — alignment.yml scripts/** trigger removal**
   (.github/workflows/alignment.yml)

   Pre-D35 push.paths and pull_request.paths listed scripts/**. The
   scripts/ directory does not currently exist (per AGENTS.md note:
   scripts/migrate-from-ocp.mjs is planned for Phase 7). A path
   filter referencing a non-existent directory has no effect on
   trigger evaluation BUT misleads readers about the workflow's
   intent. Removed from both push.paths and pull_request.paths. When
   scripts/ lands in Phase 7, the trigger should be re-added at the
   same time (see release_kit_overlay.bootstrap_quirk_policy).

**Verification — #4 (uniform X-OLP-Latency-Ms across error paths)**

#4 was found to already be correct via D32. Re-audit of all 7
in-handler sendError sites in handleChatCompletions confirmed all
attach a 5-header set via olpHeaders or olpErrorHeaders:
- 360-361 (415 wrong Content-Type) → olpErrorHeaders
- 368-369 (400 bad JSON) → olpErrorHeaders
- 378-379 (400 BadRequestError IR translation) → olpErrorHeaders
- 402-407 (503 no chain) → olpErrorHeaders
- 617-618 (503 provider disappeared) → olpErrorHeaders
- 760-761 (502 streaming pre-first-chunk error) → olpHeaders
- 778-779 (500 fallback engine error) → olpErrorHeaders
The 404 (line 922) and outer 500 (line 926) are router-level paths
without startMs context and correctly lack OLP headers. D35 adds
the #4-audit regression test pinning the 5-header invariant on the
503 no-provider response so future drift is caught immediately.

**Tests** (test-features.mjs): 416 → 424 (+8):
- #4-audit ×1 (5-header invariant on 503 no-provider sendError)
- #9 ×1 (zero-chunk streaming → 200 + Content-Type=text/event-stream
  + 5 X-OLP-* headers + [DONE])
- #10 ×1 (catch-throw after first chunk → marker + length finish + DONE)
- #10b ×1 (provider error chunk after first chunk → same recovery)
- #11a ×1 (irVersion undefined accepted)
- #11b ×1 (irVersion '1.0' accepted)
- #11c ×1 (irVersion '2.0' rejected)
- #11d ×1 (irVersion numeric 1.0 rejected)

Pre-commit fold-in (per evidence-first checkpoint #4):
- **D35 reviewer flagged JSDoc/validator drift on irVersion**
  (Suggestion #1, non-blocking). The @property typedef at
  lib/ir/types.mjs:43 said `{string} irVersion - always IR_VERSION`
  but the validator at lines 185-186 accepts `undefined`. Future
  reader who scans the @property alone sees contradiction without
  the rationale comment 140 lines below. Folded: typedef marked
  `[irVersion]` (optional) and description updated to "optional;
  when present must equal IR_VERSION ('1.0'). Pre-D35 IRs lack
  this field and remain valid."

Two other non-blocking reviewer suggestions not folded (out of
scope for D35; tracked as future polish):
- Distinct event names for the two streaming_error_after_first_chunk
  log sites (provider-emitted string vs JS exception message).
- Phase 7 TODO: re-add scripts/** trigger to alignment.yml when
  scripts/migrate-from-ocp.mjs lands.

Authority:
- ADR 0004 § Fallback safety — post-first-chunk truncation surfaces
  as `length` finish (#10 + #10b)
- ADR 0003 § IR contract — irVersion pinned to '1.0' (#11)
- AGENTS.md § Implementation status — scripts/ planned for Phase 7
  (#12)
- CLAUDE.md release_kit_overlay phase_rolling_mode — D35 lands
  under "Unreleased" against Phase 2; no version bump
- CC 开发铁律 v1.6 § 10.x — independent fresh-context reviewer
  required for code change

Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus,
independent of drafter): APPROVE_WITH_MINOR. Critical depth checks:
- Verified all 7 sendError sites in handleChatCompletions attach
  5-header set (cited line numbers reconciled with current state)
- Verified writeHead block guarded by !res.headersSent; correctly
  placed AFTER optional truncation marker, BEFORE SSE_DONE
- Verified irVersion validator strict-equality semantics across all
  4 cases (undefined / '1.0' / '2.0' / numeric 1.0)
- Verified scripts/** removed from both push and pull_request paths
- Verified hygiene: 0 hits for personal markers, home paths, OAuth
  tokens, internal IPs
- 424/424 tests pass independently in reviewer's run

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 20:20:49 +10:00
taodengandClaude Opus 4.7 ed82e65859 chore: D19 — cleanup batch (Findings 8 + 14 + 15 + D17 dead import)
cold-audit catch from 2026-05-23

Batched 4 small P3 mechanical cleanups per Iron Rule 11 IDR cleanup-batch
convention (all P3, all small, no semantic feature changes beyond defensive
validation).

Changes (7 files):

1. lib/ir/ir-to-openai.mjs (+30 / -3) — Finding 8 defensive validator:
   - Added OPENAI_FINISH_REASON_ENUM Set with the 6 spec-allowed values
     (stop / length / tool_calls / content_filter / function_call / null)
   - Added normalizeFinishReason(value) helper that returns value unchanged
     if in enum, else 'stop'
   - Routed both irChunkToOpenAISSE (streaming path) and
     irResponseToOpenAINonStream (non-stream path) through the helper
   - Bonus tightening (in-scope, same finish_reason concept):
     irResponseToOpenAINonStream's gate changed from `if (chunk.finish_reason)`
     (truthy check) to `if (chunk.finish_reason !== undefined)` so an
     explicit null (valid spec value meaning "still in progress") is no
     longer silently dropped by the truthy guard
   - Note: undefined → null collapse via `?? null` is unreachable in the
     current codebase (all provider plugins explicitly set finish_reason
     on stop chunks); defensive only against a future plugin that omits
     the field — documented inline

2. .github/workflows/alignment.yml (-16) — Finding 14 dead CI cleanup:
   - Removed `setup.mjs` from path triggers (push + pull_request) — the
     file does not exist in the repo
   - Removed the dead `KNOWN_PROVIDERS=(...)` bash array from job 1 and
     its comment block — no later step iterated over it, so the array
     was abandoned
   - LEFT untouched: the Node.js inline KNOWN_PROVIDERS array in the
     models-registry validation job — that one is actively consumed by
     the schema validation script

3. lib/providers/anthropic.mjs / codex.mjs / mistral.mjs (3 × 1 line) —
   Finding 15: removed unused `PROVIDER_ERROR_CODES` from import lines.
   Each line went from `import { ProviderError, PROVIDER_ERROR_CODES } from
   './base.mjs';` to `import { ProviderError } from './base.mjs';`. The
   constant remains exported from base.mjs (its declaration site, where
   it IS used for validation).

4. server.mjs (1 line) — D17 reviewer's observation: removed unused
   `getProviderForModel` from the import line. The function is only
   called by lib/fallback/engine.mjs which imports it directly from
   lib/providers/index.mjs. server.mjs's import was dead (the routing
   SPOT lives in engine.mjs after D17 — server.mjs uses buildDefaultChain
   exclusively).

5. test-features.mjs (+44) — Suite 3 (irChunkToOpenAISSE format) extended
   with 4 new finish_reason normalization tests:
   - Test 1: non-spec streaming finish_reason ('timeout', 'overloaded',
     'cancelled') → mapped to 'stop'
   - Test 2: spec-enum streaming finish_reason (all 6 incl. null) preserved
   - Test 3: non-spec non-stream finish_reason → mapped to 'stop'
   - Test 4: spec-enum non-stream finish_reason preserved (null
     intentionally omitted — documented inline)

Tests: 324 → 328 (+4). All pass on Node 20.

Pre-commit fold-ins (per evidence-first checkpoint #4):

- **D19 reviewer suggestion #1**: added inline comment to
  normalizeFinishReason explaining the unreachable `undefined → null`
  branch (defensive only, no current plugin omits the field). Cheap
  future-reader clarity.
- **D19 reviewer suggestion #2**: added inline comment to Test 4
  explaining why null is intentionally omitted from the spec-enum list
  (non-stream `!== undefined` gate enters with null and overwrites
  default 'stop' to null — semantically odd but spec-valid).

Reviewer suggestion #3 (consider stricter `undefined → 'stop'` on
streaming-stop path vs `null → null` on delta path) explicitly marked
out of scope by reviewer — would require call-site context awareness;
filed mentally as potential future work, not tracked as an issue
since no current path triggers it.

Authority:
- ALIGNMENT.md Rule 2(b) — only spec-defined fields in OpenAI responses
- OpenAI Chat Completions spec finish_reason enum
  https://platform.openai.com/docs/api-reference/chat/object#finish_reason
- CC 开发铁律 v1.6 § 10.x — Cold Audit Findings 8 / 14 / 15

Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus, independent
of drafter): APPROVE. Verified the unreachable `undefined → null` branch
claim by grep-checking all 3 provider plugins (none emit undefined);
verified the two KNOWN_PROVIDERS arrays were correctly distinguished
(only the dead bash one removed); ran npm test independently to confirm
328/328.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 12:16:19 +10:00
taodengandClaude Opus 4.7 (noreply@anthropic.com) dff428f3d0 docs(governance): fold in codex round-2 review findings (6 issues)
External Codex CLI review pass 2 surfaced 6 substantive issues that
round 1 fold-in missed — the self-consistency trap recurred when fold-in
was scoped only to files codex explicitly named in round 1. This commit
closes round 2 in full.

1. ADR 0002 contradicted ALIGNMENT.md (P1, codex round 2 finding 1)
   ADR 0002 still said "three default-enabled (Anthropic, OpenAI Codex,
   Mistral Vibe)" while ALIGNMENT.md (post round 1) said v0.1 ships zero
   Enabled Providers. Accepted ADR contradicted constitution.
   Fix: ADR 0002 + ADR 0001 + docs/adr/README.md index rewritten to
   Candidate framing.

2. release.yml would publish stale v0.1.0-bootstrap notes (P1, round 2
   finding 2)
   The "Unreleased" amendments would have been silently dropped on tag
   push because release.yml extracts only the matching version section.
   Fix: CHANGELOG restructured so the amended state IS the v0.1.0-
   bootstrap section. Full review history (opus + 2 codex rounds)
   captured inline.

3. package.json advertised non-existent entrypoints (P2, round 2
   finding 3)
   main/scripts.test/scripts.start pointed to files that do not exist.
   Local npm test and npm start failed; CI masked.
   Fix: remove all three from package.json. They return in Phase 1
   alongside the real files. test.yml bootstrap-tolerance updated to
   also skip when scripts.test is absent.

4. models-registry.json missing despite SPOT claim (P2, round 2
   finding 4)
   Fix: minimal stub committed (version + empty providers map).
   alignment.yml validator now actually runs.

5. alignment.yml commit-citation soft check Bash subshell trap (P2,
   round 2 finding 5)
   git log ... while read ... WARN=1 — the while loop ran in a subshell
   because of the pipe, so WARN never propagated out. The post-loop
   check always reported "clean" even when warnings fired.
   Fix: process substitution done less than less than (git log ...).

6. Tier A "permanent" wording inconsistent across ADR 0006 + alignment.
   yml workflow text (P3, round 2 finding 6)
   Fix: unified to "Excluded by default with no routine reinstatement
   path; re-inclusion requires ADR 0006 supersession or amendment with
   new primary-source evidence."

Reviewer: OpenAI Codex CLI (external, fresh-context, pass 2). Iron Rule
10 satisfied — round 2 reviewer was not the implementer of round 1
fold-in.

Memory learning updated: the self-consistency trap recurs in the fold-in
step. Future fold-ins must grep the entire repo for the concept, not
only edit files the reviewer named. See learnings/ai_reviewer_self_
consistency_trap.md in cross-machine memory.

Co-Authored-By: Claude Opus 4.7 (noreply@anthropic.com)
2026-05-23 16:35:23 +10:00
taodengandClaude Opus 4.7 c5777aa4d7 fix(ci): correct bootstrap-tolerance gate in test.yml
The bootstrap commit's test.yml had an incorrect skip condition. The
intent was "if no test-features.mjs, skip" — but the actual logic was
"skip only if no test-features.mjs AND no npm test script in
package.json." Since package.json declares `scripts.test`, the second
check returned true and the gate never fired; `npm test` ran and
failed with `Cannot find module test-features.mjs` (verified at
GitHub Actions run 26324988738 on the bootstrap commit).

Fix: drop the second clause. The file's presence is the only correct
gate — the npm script is always present in package.json from day one,
so checking it adds no information. Comment makes the bootstrap-vs-
Phase-1 lifecycle explicit so future readers don't reintroduce the
two-clause guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:50:27 +10:00
taodengandClaude Opus 4.7 26b928ec13 fix(ci): replace heredoc with echo statements in alignment.yml
The bootstrap commit had alignment.yml using a heredoc to print the
ALIGNMENT GUARDRAIL FAILURE banner. The independent reviewer flagged
that bash required the closing EOF at column 0; moving EOF to column 0
fixed the bash parse but broke YAML parsing (EOF at column 0 became a
top-level mapping key, which is invalid YAML).

GitHub Actions rejected the workflow with "This run likely failed
because of a workflow file issue" — verified locally via
`ruby -ryaml -e 'YAML.safe_load(File.read(".github/workflows/alignment.yml"))'`
which reproduced the Psych::SyntaxError at line 126.

Fix: drop the heredoc entirely. Use a series of echo statements
inside the bash run block, all at YAML's required 10-space indent.
This:
  - keeps the structured ALIGNMENT GUARDRAIL FAILURE banner visible
    when the gate trips (preserving the original UX intent);
  - is unambiguous to YAML's parser (no heredoc-vs-indent conflict);
  - is unambiguous to bash (no heredoc-EOF indent rules to remember).

The § character in "ALIGNMENT.md § Risk Tier" is emitted as the
UTF-8 byte sequence \xc2\xa7 to keep the bash literal portable across
locale settings; runners may not have a UTF-8 locale by default.

Verified all three workflow YAMLs parse with Ruby's Psych:
  YAML valid
  release.yml valid
  test.yml valid

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:49:09 +10:00
taodengandClaude Opus 4.7 0041fb1017 chore: bootstrap OLP v0.1 — multi-provider LLM proxy
Initial release. OLP (Open LLM Proxy) is a personal- and family-scale
multi-provider LLM proxy that supersedes OCP (Open Claude Proxy).

Trigger: Anthropic's 2026-05-14 announcement (effective 2026-06-15)
moves `claude -p` / Agent SDK / third-party agent traffic out of the
Pro/Max subscription pool into a separate fixed monthly Agent SDK
Credit pool. OCP's foundational assumption ("subscription = unlimited
within rate limits") breaks for Anthropic on that date. Spreading
risk across multiple providers is the structural response.

Phase 0 lands:
- ALIGNMENT.md (constitution: 5 Rules, 3 Authorities, 4-tier Risk
  Framework, 8-provider inventory)
- AGENTS.md (multi-tool agent guidelines; inherits cc-rules)
- CLAUDE.md (Claude-Code session instructions + release_kit overlay)
- README.md (phase-aware skeleton)
- docs/adr/0001-0006 (Founding ADRs: project founding / plugin
  architecture / IR design / fallback engine / cross-provider cache /
  provider inclusion + risk-tier framework)
- .github/PULL_REQUEST_TEMPLATE.md (8-radio Change Type + per-type
  Authority Evidence + Iron Rule 10 reviewer checklist)
- .github/workflows/alignment.yml (blacklist + Antigravity exclusion
  enforcement + models-registry validator + commit-citation soft check)
- .github/workflows/release.yml (auto-release on tag with version
  match check per Iron Rule 5)
- .github/workflows/test.yml (Node 20/24 matrix, bootstrap-tolerant)
- package.json, .gitignore, LICENSE (MIT), CHANGELOG.md

Provider inventory at bootstrap:
  Tier D (default-enabled):       anthropic, openai, mistral
  Tier C (opt-in):                grok, kimi
  Tier B (opt-in + consent):      minimax, glm, qwen
  Tier A (permanently excluded):  google-antigravity

Supersedes OCP ADR 0005 (No Multi-Provider) per OLP ADR 0001. OCP
will enter maintenance mode when OLP v0.1 ships per Phase 7 plan.

Iron Rule 10 gate: fresh-context independent opus reviewer audited
all 15 governance files against OLP v0.1 spec + OCP precedent.
Verdict: APPROVE_WITH_MINOR. Two minor findings folded in:
  1. alignment.yml heredoc EOF moved to column 0 (was indented;
     bash parse failed silently on real blacklist hits, printing
     a cryptic "syntax error" instead of the structured ALIGNMENT
     GUARDRAIL FAILURE banner).
  2. AGENTS.md clarified that the SPOT discipline for
     models-registry.json will be codified by a Phase-1 ADR (OLP
     ADR 0003 is currently the IR design, not a SPOT codification;
     OCP's ADR 0003 is the precedent but OLP's registry shape
     differs and warrants its own ADR).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:46:56 +10:00