From 7fee250f1ea7ac41805d1ee8503dd0457c2e5ed9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 06:09:32 +0000 Subject: [PATCH 1/7] docs: record the 2026-09-02 product-goal-directive revision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner reissued the full nine-section autonomous PR/merge/development loop directive with substantially expanded text (core/consumer boundary policy in §2, i18n and Keyverse-scoping detail in §4, ontology-ownership split and broadened identifier-naming rule in §5, an explicit orchestrator/free pin folded into §8, and a full named core-repo table with a core-vs-domain-product classification in §9). Recorded verbatim per this file's own conflict policy. Verified all 29 repos named in §5/§9 exist under ContextualWisdomLab with exact-matching case, and reconciled the new §5 identifier-naming rule against CWL-MASTER-CONTEXT.md §7's DB-object grandfather clause (still binding, now with an explicit note that the broader rule applies going forward on touched/new code, not as a mandate to force-rename existing identifiers ecosystem-wide). Flagged, but did not resolve, an open tension between §9's quarantine-sandbox-runtime repo and CWL-MASTER-CONTEXT.md §3's description of noema owning the sandbox — needs each repo's own docs read before deciding, not guessed. Nine-section transcription independently verified byte-for-byte against the source text by a separate review pass before this commit. See docs/doctoring/product-goal-directive.md for the full reconciliation record. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN --- docs/doctoring/product-goal-directive.md | 163 +++++++++++++++++++++ docs/product-goal-directive.md | 175 +++++++++++++++++------ 2 files changed, 294 insertions(+), 44 deletions(-) diff --git a/docs/doctoring/product-goal-directive.md b/docs/doctoring/product-goal-directive.md index d752033602..189c5aa33c 100644 --- a/docs/doctoring/product-goal-directive.md +++ b/docs/doctoring/product-goal-directive.md @@ -86,3 +86,166 @@ fixed: conventions this record reconciles against. - ContextualWisdomLab/.github#1429 — the PR carrying this change and Devin Review's findings. + +## 2026-09-02 revision + +- **Subject:** the owner reissued the full nine-section directive with + substantially expanded text in every section (delivered directly in an + agent session, not via a PR comment). Per this file's own conflict policy + ("edit this file in place... do not fork a second copy elsewhere"), + `docs/product-goal-directive.md` was rewritten in place to hold the new + text verbatim, superseding the 2026-08-30 wording of all nine sections. + +### What changed in the directive text + +- §2 gained an explicit "build the immature core, don't bypass it" policy: + a consumer repo must not duplicate, work around, or exclude functionality + that belongs to an immature core/owner repo — it must develop the missing + piece at the owner (RED test → contract → feature → docs → release) and + only then consume the new versioned release. Exclusion is only valid when + the boundary itself is wrong or there is no real shared demand, and must + be recorded as an ADR. +- §2 also added a new Korean-copyediting tool reference, + `https://github.com/epoko77-ai/im-not-ai`, for preserving meaning/facts/ + figures/proper nouns while polishing Korean prose. This repo is under a + different GitHub account (`epoko77-ai`), not `ContextualWisdomLab` — noted + inline in the directive so a future agent doesn't mistake it for an org + core repo or try to add it via the org's repo-scope tooling as if it were + one. +- §4 (renamed from "UX/UI and customer-facing expression" to "UX/UI, i18n, + and customer-facing expression") added: UI-as-composed-reusable-objects, + `shadcn/ui` as a component source (not a Storybook substitute), an + explicit unpinned frontend stack (React/Vite/shadcn/ui/jQuery 4 chosen on + merit, not mandated), Keyverse's role narrowed to an auth backend only + (Direct Grant/ROPC or the Keycloak REST API) with product-owned + login/signup/recovery forms, and a detailed i18n policy: 8 supported + languages (ko/en/ja/zh/vi/es/de/fr), CJK/width/line-break/font-fallback/ + locale-format testing per language in Storybook and E2E, and — the + highest-signal addition — translation strings must live as a DB-backed + **versioned resource**, not a file or JS bundle; server/native code + fetches only the screen keys it needs and caches them; the browser never + loads a full catalog or heavy i18n JavaScript, and an SPA is not assumed. + If no shared translation-management product exists yet, a new repo should + provide per-product translation CRUD, review/approval, deploy, rollback + API, and an admin UI. +- §5 (renamed to "Architecture, ontology, naming, and database conventions") + added an ontology-ownership split across four named repos — + **ConceptWeave** (observe→discover→propose→align→validate→review→publish + and semantic release), **semantic-data-portal** (catalog/governance/ + consumption), **context-graph-contracts** (interoperability contracts), + **enterprise-architecture-core** (Context Map and cross-org decisions) — + with domain truth and Ubiquitous Language staying with each product + owner, and every concept/relation/dimension/measure/mapping published as + an immutable release carrying evidence/provenance/validity/confidence/ + status/deprecation/locale-label metadata; consumers may only use released + API/contracts through an ACL, never copy files, run cross-service SQL, or + publish without authorization. UI-translation and ontology-label ledgers + are explicitly kept separate (the two i18n-shaped systems in §4 and §5 + are not the same system). §5's identifier-naming rule also broadened from + "DB object names" to essentially every code identifier (variables, + constants, parameters, fields, functions, methods, classes, types, + modules, packages, APIs, DB objects, files, directories) — see + Reconciliation below. +- §8 folded the pool pin directly into the quoted text ("`orchestrator/free` + 고정"), removing the prior section's ambiguity outright rather than + needing an out-of-band note to resolve it. +- §9 (renamed from "Reference libraries and tool invocations" to "Core + foundation and development/consumption boundaries") replaced the general + reference-library bullet list with an explicit table of 21 named + core-owner repos grouped by responsibility (workflow/review/security/ + release; enterprise architecture/context contracts; ontology generation + vs. catalog/governance; model orchestration/OIDC/evidence; identity; + outbound/browser/edge/sandbox isolation; batch/embedding; psychometrics + measurement kernels; retrieval/threading; editor/diagram tooling; MHTML + ETL; SAST/gateway), plus an explicit classification of seven repos + (`naruon`, `LineageWeave`, `psychometrics-commons`, `disksage`, + `PolicyWeave`, `CalendarWeave`, `supply-chain-control-plane`) as domain + product/composition **consumers**, not core, with the instruction that + genuinely shared functionality found duplicated across consumers should + be extracted to a core owner and developed through integrated CI. + +### Verification performed this revision + +1. **Repo-existence check.** Every one of the 29 repositories named across + §5 and §9 (the four ontology repos, all 21 §9 core repos, and the seven + §9 domain-consumer repos) was checked against a live listing of every + `ContextualWisdomLab` repository the operating account can reach. All 29 + matched exactly, including case (e.g. `ConceptWeave`, `EgressWeave`, + `OriginWeave`, `DiagramWeave`, `PolicyWeave`, `CalendarWeave` — all + PascalCase, confirmed correct as given, not "corrected" to another + case). None needed a spelling or case fix. This check is recorded + verbatim in the directive file itself (§9 "Verification" note) so a + future agent doesn't have to re-derive it from this doctoring record. +2. **Conflict check against `docs/CWL-MASTER-CONTEXT.md`.** Two real + tensions were found and handled per this file's stated conflict policy + ("resolve the conflict and update whichever document is wrong — do not + silently pick one"): + - **§5 naming scope vs. §7's DB-object grandfather clause (resolved).** + `docs/CWL-MASTER-CONTEXT.md` §7 is narrow and binding: *"DB object + names = 2+ word snake_case (don't rename existing Camel/Pascal)."* + The revised §5 is broader (every code identifier, not just DB + objects) and says a violating name "gets replaced" — read alone and + literally, combined with this org's full-autonomy convention, an + agent could take that as a mandate to sweep every repo and + force-rename every existing identifier that doesn't fit, which would + be a large, high-risk, potentially breaking action with no ADR and no + migration plan, and would directly contradict §7's explicit + grandfather clause for the DB-object case it already covers. Added a + reconciliation note directly under §5 in the directive (not inside + the quoted text) stating: the broader rule applies going forward, on + code an agent is already touching or creating, not as license to + force-rename existing identifiers ecosystem-wide; §7's DB-object + grandfather clause remains binding and unambiguous; a genuinely + warranted repo-wide rename needs its own ADR and migration plan, not + a blanket action under this directive. + - **§9 quarantine-sandbox ownership vs. `docs/CWL-MASTER-CONTEXT.md` §3 + (flagged, not resolved).** §3 currently states `noema` owns "the + lightweight quarantine sandbox." The revised §9 lists + `quarantine-sandbox-runtime` as its own dedicated repo, grouped with + `EgressWeave`/`OriginWeave`/`pingora-gateway` under "outbound·browser· + edge·격리 core." Whether sandbox ownership moved to the new dedicated + repo, is now shared between the two repos, or `quarantine-sandbox-runtime` + covers a different scope than "the lightweight quarantine sandbox" in + §3, was **not** verified against either repo's actual README/ + ARCHITECTURE content — doing so honestly requires reading both repos, + which this pass did not do, rather than guessing. Recorded as an open + item directly in the directive file (§9 "Open reconciliation item") + instead of silently picking an answer. A future pass should read both + repos and either update §3, update §9, or record an ADR if the split + is a genuinely new architectural decision. + - `docs/CWL-MASTER-CONTEXT.md` §3 was **not** otherwise updated this + pass to add the ~13 §9 core repos it doesn't yet mention + (`ConceptWeave`, `enterprise-architecture-core`, + `context-graph-contracts`, `EgressWeave`, `OriginWeave`, + `pingora-gateway`, `quarantine-sandbox-runtime`, `EmbedRelay`, + `DiagramWeave`, `mhtml-etl-gateway`, `PolicyWeave`, `CalendarWeave`, + `supply-chain-control-plane`, `psychometrics-commons`, + `LineageWeave`) — that is a larger, separate reconciliation (each + repo's actual role description in §3 should be written from that + repo's own README/ARCHITECTURE, not paraphrased from this directive's + one-line-per-repo summary) and is intentionally left as future work + rather than rushed. +3. **§8 pool-pin state, re-verified against the actual workflow files** + (not just ADR-0003's prose): `opencode-review-dispatch.yml`, + `noema-review.yml`, `strix.yml`, and `pr-review-autofix.yml` in + `ContextualWisdomLab/.github` all hardcode + `contextual-orchestrator/orchestrator/free` (or `orchestrator/free` for + the Noema sidecar), each with fail-closed validation rejecting any other + value. §8's new explicit "`orchestrator/free` 고정" wording matches this + confirmed reality exactly — no further action needed for this section. + +### Audit trail (2026-09-02 revision) + +- `docs/product-goal-directive.md` — the rewritten directive (all nine + sections) and its inline 2026-09-02 reconciliation/verification notes + under §2, §5, §8, and §9. +- `docs/CWL-MASTER-CONTEXT.md` §3, §7 — the documents this revision's + reconciliation checks were run against; §3 still needs the larger, + separate follow-up noted above. +- `.github/workflows/opencode-review-dispatch.yml`, + `.github/workflows/noema-review.yml`, `.github/workflows/strix.yml`, + `.github/workflows/pr-review-autofix.yml` — the live workflow files + checked for the §8 pool-pin verification. +- `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md` — the + authoritative ADR for *why* the pool pin is `orchestrator/free`; this + revision's §8 note points here rather than restating the reasoning. diff --git a/docs/product-goal-directive.md b/docs/product-goal-directive.md index c76c4226e4..5929b02b21 100644 --- a/docs/product-goal-directive.md +++ b/docs/product-goal-directive.md @@ -1,8 +1,9 @@ # Product goal directive — autonomous PR/merge/development loop -**Status:** active standing directive · **Owner intent recorded:** 2026-08-30 · **Scope:** the full -ContextualWisdomLab ecosystem (every repo an agent can reach from this org, leveraged in order of -product responsibility / reuse boundary / docs / implementation / consumption — not by name). +**Status:** active standing directive · **Owner intent recorded:** 2026-08-30, revised 2026-09-02 · +**Scope:** the full ContextualWisdomLab ecosystem (every repo an agent can reach from this org, +leveraged in order of product responsibility / reuse boundary / docs / implementation / consumption — +not by name). ## Why this file exists @@ -23,64 +24,130 @@ is wrong — do not silently pick one. The directive is recorded verbatim (Korean, as authored) in the nine sections below, each given a short English heading for navigability. Do not paraphrase or shorten these sections when copying them -elsewhere; link to this file instead. +elsewhere; link to this file instead. The 2026-09-02 revision replaced the full text of all nine +sections (the owner reissued the directive with substantially expanded detail in §2, §4, §5, §8, and +§9); see "Revision history" at the end of this file and `docs/doctoring/product-goal-directive.md` for +the reconciliation notes on both the original and this revision. ## 1. Execution goal and continuous loop -> 실행 목표와 지속 Loop 열린 PR마다 별도 중간 보고 없이 리뷰 확인→수정→GitHub Checks 재검증→병합→다음 개발을 반복하라. PRD를 읽고 Loop·Goal을 자율 생성·수정·제거해 PR을 병합 또는 0개로 만들며 상용화하라. 200억 달러에 판매할 자신이 있을 품질과 구매자가 체감할 제품 Gap 해소가 목표다. ADR·리서치·현행 데이터·PR로 기능 명세·PRD·TRD·UML·Gap·조치 상태를 도출해 docs/product-technical-gap-baseline.md에 갱신하라. 한 시간 간격으로 예약하고 메시지도 개선·갱신하라. PR·Issues 소진 후에도 제품 Gap 개발과 병합 Loop를 계속한다. 내가 온전히 소유한 ContextualWisdomLab 저장소를 레버리지 순으로 연계해 PR 병합·추가와 Connector 추가·수정 등 Ecosystem을 구축하라. Ecosystem 전 라이브러리 PRD를 숙지하고 조직·저장소명 대소문자를 지킨다. 리뷰·Checks 대기는 Blocker가 아니며, 실패 원인·수정·재실행 필요에 즉시 대응하며 안전한 작업을 계속한다. 결과 보고에 멈추지 말고 다음 Loop로 이동하라. 저장소는 이름이 아니라 제품 책임·재사용 경계·문서·구현·소비 저장소를 대조해 선택한다. ADR·Goal을 수시로 갱신하고 Goal 수정 불가 시 Loop를 갱신한다. +> 실행 목표와 지속 Loop 열린 PR마다 보고 없이 리뷰→수정→Checks 재검증→병합→다음 개발을 반복하라. PRD로 Loop·Goal을 조정해 PR을 병합하거나 0개로 만들며 상용화한다. 목표는 200억 달러 판매 품질과 고객 체감 Gap 해소다. ADR·리서치·현행 데이터·PR에서 PRD·TRD·UML·Gap·조치를 도출해 docs/product-technical-gap-baseline.md에 갱신하라. 매시간 예약 메시지를 개선한다. PR·Issues 소진 뒤에도 Gap 개발·병합과 ContextualWisdomLab 저장소·Connector 연계를 계속한다. 전 라이브러리 PRD와 조직·저장소명 대소문자를 지킨다. 리뷰·Checks 대기는 Blocker가 아니다. 실패를 즉시 수정·재실행하고 다른 안전한 일을 계속한다. 저장소는 책임·재사용 경계·구현·소비 관계로 고르고 ADR·Goal·Loop를 갱신한다. ## 2. Concurrent operation, PR handling, and root-cause fixes -> 동시 작업·PR 운영·근본 수정 원격 Agent의 동시 Commit·Push를 경합으로 단정해 Force Push·중단하지 말고 변경 취지·이유를 확인해 이어간다. Commit·Push 전 병합 여부를 확인하고 삭제 근거를 남긴다. Self-modifying/Source-fix Workflow는 목적 달성 후 삭제하고 잔존 시 관찰·제거한다. 가능한 PR은 Stack하고 not-merge-ready를 merge-ready로 전환한다. 유관 프로젝트 원인이 엮이면 함께 처리하고 Stacked PR을 중앙 OpenCode Agent가 리뷰하지 않으면 ContextualWisdomLab/.github를 수정한다. Agent 간 대화·Spawn을 활용한다. 수동 해법은 모두 코드·설정에 반영한다. PYTHONPATH=. 누락은 설정하고 GitHub Actions·런타임 오류는 로그·Root Cause Analysis로 제거한다. 전체 GitHub Checks 실패를 확인·수정한다. ContextualWisdomLab 내부 라이브러리 문제라면 원시 공급자 오류까지 고쳐 PR한다. 개발 프로세스에 https://github.com/DietrichGebert/ponytail 및 https://github.com/obra/superpowers 를 사용하되 superpowers의 "무조건 질문" 규칙은 무시한다. https://github.com/tirth8205/code-review-graph 와 https://github.com/colbymchenry/codegraph 도 사용하고 인덱싱은 스스로 수행한다. 이는 명시적으로 허가됐다. +> 동시 작업·PR 운영·근본 수정 동시 Commit·Push를 경합으로 단정하거나 Force Push·중단하지 말고 취지를 이어간다. Commit 전 병합·삭제 근거를 기록하고 목적을 마친 Self-modifying/Source-fix Workflow는 삭제한다. PR은 Stack해 merge-ready로 만들고 유관 원인도 처리한다. Stacked PR의 OpenCode 리뷰가 빠지면 .github를 고친다. Agent 간 대화·Spawn을 쓴다. 수동 해법은 코드·설정화한다. PYTHONPATH=.·Actions·runtime 오류는 로그 RCA로 제거한다. 내부 문제는 canonical owner·원시 공급자까지 고친다. 필요한 core가 미성숙해도 consumer에서 복제·우회·제외하지 말고 owner에 RED test·계약·기능·문서·release를 개발해 통합 CI GREEN 후 versioned release로 연결한다. 경계가 틀리거나 공통 수요가 없을 때만 ADR 근거로 제외한다. DietrichGebert/ponytail·obra/superpowers를 쓰되 "무조건 질문"은 무시한다. tirth8205/code-review-graph·colbymchenry/codegraph도 인덱싱한다. 한국어 문구·문서·번역은 https://github.com/epoko77-ai/im-not-ai로 의미·사실·수치·고유명사를 보존하며 윤문한다. -## 3. Research, standards, and documentation traceability - -> 연구·표준·문서 추적성 모든 개발은 최신 권위 국제 표준·논문을 조사해 APA 7th로 인용하고 doctoring에 기록하며 누락 근거를 보충한다. Local Zotero API가 되면 기존 자료를 읽거나 OA 논문을 추가한다. 논문·표준은 exact-head·전체 PR·내부 모듈·API에 모순 없이 결합하고 충돌을 수정한다. AGENTS.md, CLAUDE.md, ARCHITECTURE.md, CHANGELOG.md 등 ADR 문서를 상시 갱신하고 Core ERD, UML, PRD, TRD, user stories, storyboard, wireframes, Storybook inventory, security·test·operability baseline 및 필요한 그림을 포함한다. 릴리즈 가능하면 버전을 올려 배포하고 CHANGELOG.md를 갱신한다. GitHub.io를 언급하려면 페이지를 실제 출판한다. - -## 4. UX/UI and customer-facing expression - -> UX·UI와 고객 표현 필요하면 Figma와 Storybook(https://github.com/storybookjs/storybook), https://github.com/nextlevelbuilder/ui-ux-pro-max-skill, https://github.com/local-over/Anti-Slop-UI 를 함께 쓴다. 반복 웹 객체는 디자인 토큰화·모듈화하고 Figma File ID를 ADR에 기록한다. Storybook 장면별·Edge case별 Event를 조사·구현한다. UX·UI는 반드시 스크린샷으로 검수하고 ui-ux-pro-max로 Accessibility, Touch & Interaction, Performance, Style Selection, Layout & Responsive, Typography & Color, Animation, Forms & Feedback, Navigation Patterns, Charts & Data를 정의·검토·반영·적용·감사한다. 내부 구현 경계를 고객 화면에 노출하지 않고 문구로 고객의 다음 행동을 돕는다. Frontend는 디자인 토큰 CSS, 버튼 Action Edge, Interaction UX, i18n 번역 일관성까지 테스트한다. - -## 5. Architecture, naming, and database conventions - -> 아키텍처·명명·데이터베이스 소프트웨어는 중앙 .github, naruon, 다른 저장소와 연결 가능하게 만든다. DDD를 적용해 핵심·지원·일반 Subdomain, Bounded Context, Context Map, Ubiquitous Language를 ADR에 정의하고 Aggregate·Entity·Value Object·Domain Service·Repository·Domain Event·Invariant를 코드·API·DB·테스트에 일치시킨다. Aggregate는 최소 트랜잭션 경계로 두며 외부·레거시는 Anti-Corruption Layer로 격리하고 Shared Kernel은 최소화한다. 단독·반입 모듈 모두 우수한 모듈러 MSA를 지향하고 단일 소프트웨어가 Monolithic Architecture처럼 비대해지면 책임 경계에 따라 저장소를 분리한다. 소프트웨어명과 내부 호출자·클래스명이 다르거나 옛 이름(예: wardnet)을 쓰면 정식 이름으로 바꾼다. DB 객체명은 두 단어 이상의 snake case, Carmel case 또는 pascal case여야 하고 snake case를 우선한다. 위반명은 전부 치환한다. DB는 제3정규화와 Hot Partition 대비를 준수한다. Lock을 관리하고 불가하면 Read/Write DB를 분리한다. 영속화 경로의 항목별 UPSERT를 추적하고 없으면 계약을 보강한다. 명시적 구매자가 없는 제품은 코드 안팎의 Buyer를 정상 객체명으로 바꾼다. CSAP·SOC 2 인증을 고려한다. PII Masking이 업무를 마비시키므로 규정 준수형 비Masking 보호 대안을 설계한다. 실데이터 테스트·개발의 인명·기관명은 코드·ADR에서 익명화한다. GitHub Secrets의 PYPI API Key와 대부분 Public 배포라는 전제를 반영한다. - -**Reconciliation (flagged by Devin Review on this PR, 2026-08-30 — see `docs/doctoring/product-goal-directive.md`):** taken verbatim and cross-referenced against the rest of the ecosystem's own naming history, this section's quoted text reads backwards in two places: +**Note (2026-09-02):** `epoko77-ai/im-not-ai` is a repository under a different GitHub account +(`epoko77-ai`), not `ContextualWisdomLab` — it is an external Korean-copyediting tool referenced by +URL, not an org-owned core repo, and is not part of the §9 core-repo list below. -- It names "wardnet" as an example of an "old name" (옛 이름) to rename *away from*. But per `docs/CWL-MASTER-CONTEXT.md` §3/§10, `waf-ids-ai-soc` → **wardnet** is an already-completed rename — wardnet is the current canonical product name, not a legacy one. Read this section's "old name" example as applying to whatever pre-rename name a component still uses internally (stray `waf-ids-ai-soc` references, say), never as license to rename wardnet itself away from its current name. -- "위반명은 전부 치환한다" ("replace all violating [DB object] names") would, read literally, force-rename existing CamelCase/PascalCase database objects. That contradicts the binding convention in `docs/CWL-MASTER-CONTEXT.md` §7: *"DB object names = 2+ word snake_case (don't rename existing Camel/Pascal)."* The §7 rule governs: 2+-word snake_case is required for **new** DB objects; existing CamelCase/PascalCase objects are grandfathered and must not be force-renamed. +## 3. Research, standards, and documentation traceability -Per this file's own conflict policy above: this note is the resolution, and `docs/CWL-MASTER-CONTEXT.md` §7 is the document that was right — do not force-rename wardnet or existing Camel/Pascal DB objects on the strength of this section's verbatim wording alone. +> 연구·표준·문서 추적성 최신 권위 표준·논문을 조사해 APA 7th로 인용하고 doctoring에 남긴다. Local Zotero API가 되면 기존 자료나 OA 논문을 보강한다. 근거는 늘 exact-head·PR·모듈·API에 연결하고 충돌을 고친다. AGENTS.md·CLAUDE.md·ARCHITECTURE.md·CHANGELOG.md·ADR, ERD·UML·PRD·TRD·user story·storyboard·wireframe·Storybook·security/test/operability baseline을 갱신한다. 릴리즈 가능하면 버전·CHANGELOG를 올려 배포하고 GitHub.io는 실제 출판한다. 의사결정은 맥락을 잊거나 처음 보는 사람도 문제·제약·대안·선택/기각 이유·근거·위험·효과·후속 조치를 재구성하도록 구체적이고 자세히 기록한다. 결론·전제를 생략하지 말고 사용자·운영·장애 장면이 떠오를 사례와 증거를 exact-head·로그·이슈·PR·ADR·실험에 연결해 다른 Agent가 검증·계속하게 한다. + +## 4. UX/UI, i18n, and customer-facing expression + +> UX·UI·i18n과 고객 표현 Figma·Storybook·ui-ux-pro-max·Anti-Slop-UI를 쓴다. UI는 모두 재사용 객체이고 페이지는 조합으로 만든다. token·Figma ID를 ADR에 남긴다. Storybook에서 정상·로딩·빈·오류·권한·반응형·상호작용 상태를 격리 개발·문서화하고 스크린샷·E2E로 접근성·터치·성능·타이포그래피·색상·폼·탐색·차트를 감사한다. shadcn/ui는 component source로 Storybook과 대체 관계가 아니다. Frontend stack은 고정하지 않으며 React·Vite·shadcn/ui·jQuery 4 등은 보안·유지보수·표준·접근성·성능을 충족할 때 쓴다. 내부 경계를 숨기고 다음 행동을 안내한다. Keyverse는 인증 backend로 유지하되(Direct Grant/ROPC 또는 Keycloak REST API), 로그인·가입·복구는 제품 자체 form으로 만든다. token CSS·Action Edge·Interaction UX를 검증한다. i18n은 한국어·영어·일본어·중국어·베트남어·스페인어·독일어·프랑스어를 지원한다. UI 폭·줄바꿈·CJK·텍스트 팽창·font fallback·locale 형식을 고려하고 언어별 Storybook·E2E로 잘림·겹침·의미 축약을 막는다. 번역 원장은 파일·JS bundle이 아닌 DB의 versioned resource다. server/native가 화면 key만 조회·cache하며 browser에 전체 catalog·무거운 i18n JavaScript를 싣지 않고 SPA를 전제하지 않는다. 공통 관리 제품이 없으면 새 저장소를 만들어 제품별 번역·검토·승인·배포·rollback API·관리 UI를 제공한다. + +## 5. Architecture, ontology, naming, and database conventions + +> 아키텍처·온톨로지·명명·데이터베이스 DDD의 Subdomain·Bounded Context·Context Map·Ubiquitous Language와 Aggregate·Entity·Value Object·Domain Service·Repository·Domain Event·Invariant를 ADR·코드·API·DB·test에 일치시킨다. Aggregate는 최소 transaction 경계로 두고 외부·legacy는 ACL로 격리하며 Shared Kernel을 최소화한다. 모듈러 MSA를 지향하고 비대한 Monolith는 책임별로 분리하며 옛 이름을 고친다. 통합 온톨로지는 ConceptWeave가 observe→discover→propose→align→validate→review→publish와 semantic release를, semantic-data-portal이 catalog·governance·소비를, context-graph-contracts가 상호운용 계약을, enterprise-architecture-core가 Context Map·결정을 맡는다. domain truth·Ubiquitous Language는 제품 owner에 남긴다. 개념·관계·dimension·measure·mapping은 evidence·provenance·validity·confidence·status·deprecation·locale label을 가진 immutable release로 배포한다. consumer는 released API/contract·ACL만 쓰고 파일 복사·cross-service SQL·미승인 publication을 금지한다. UI 번역과 ontology label의 원장은 분리한다. 변수·상수·매개변수·필드·함수·메서드·클래스·타입·모듈·패키지·API·DB 객체·파일·디렉터리는 두 단어 이상 snake_case·camelCase·PascalCase로 명명하고 snake_case를 우선한다. 언어·framework·외부 계약 관례는 경계에서 변환하고 위반명은 치환한다. DB는 3NF·Hot Partition 대비·Lock·필요시 Read/Write 분리·항목별 UPSERT를 지킨다. placeholder Buyer는 실제 도메인명으로 바꾼다. CSAP·SOC 2를 고려한다. PII Masking이 업무를 마비시키면 준수형 비Masking 대안을 설계한다. 실데이터 인명·기관명은 익명화하고 PYPI API Key·Public 배포 전제를 반영한다. + +**Reconciliation (naming scope, 2026-09-02 — supersedes the 2026-08-30 note below for this point):** +this section's identifier-naming rule is now explicitly broader than the original directive's — it +covers every code identifier (variables, functions, classes, modules, files, directories, API and DB +object names), not DB objects alone, and says a violating name "gets replaced" (위반명은 치환한다). +Read together with `docs/CWL-MASTER-CONTEXT.md` §7's binding, narrower rule — *"DB object names = 2+ +word snake_case (don't rename existing Camel/Pascal)"* — the two do not contradict as long as this +broader rule is applied the same way: **going forward, on code an agent is already touching or +creating**, not as a mandate to sweep the ecosystem and force-rename every existing identifier that +doesn't fit. A repo-wide, blind rename of existing public APIs, classes, or DB objects is a +high-blast-radius, potentially breaking change for real consumers — exactly the kind of action this +org's own engineering conventions (minimal, reviewed, evidence-based changes; ADR-recorded reasoning) +guard against. §7's DB-object grandfather clause remains binding and unambiguous: existing Camel/Pascal +DB objects are not renamed on the strength of this section alone. Where a genuinely repo-wide rename is +warranted (a name is actively misleading or the repo/product itself was renamed), record it as an ADR +with its own migration plan, not as a blanket action under this directive. + +**Note (2026-08-30, on the original wording, retained for history):** the original (2026-08-30) +version of this section named "wardnet" as an example of an "old name" (옛 이름) to rename *away +from*. Per `docs/CWL-MASTER-CONTEXT.md` §3/§10, `waf-ids-ai-soc` → **wardnet** is an already-completed +rename — wardnet is the current canonical product name. The 2026-09-02 revision above no longer +contains that specific wording, and §9 below lists wardnet as a current core-repo owner, consistent +with treating it as the canonical name. If a future revision of this directive reintroduces "wardnet" +as an old-name example, treat that as the same error this note originally corrected, not as new intent +to rename the product. ## 6. Implementation language, computation, and measurement principles -> 구현 언어·연산·측정 원칙 Docstring Coverage, Test Coverage, Edge Case Test Coverage를 각각 100%로 만든다. 초보자가 별도 코드 분석 없이 이해할 수 있을 만큼 충분한 docstring을 제공한다. 수리과학, Psychometrics, Exploratory Data Analysis, 데이터과학의 모든 core 연산 레이어는 Python으로 구현하지 말고 무조건 Rust로 작성한다. Vector 연산, Linear Algebra, Matrix Algebra, LLM token size 연산도 포함한다. GPU와 CPU multithreaded 실행을 지원하고 context switching을 최소화한다. 속도·안정성·보안이 중요한 일반 소프트웨어도 Rust를 사용하며, 기존 타 언어 구현은 전환·리팩터링하거나 명확한 Rust API Call 경계로 분리한다. 확률표집 계약에는 표본 설계, 오차 목표, 실패 분모를 명시해 ADR과 감사 코드에 반영한다. Atomistic fallacy를 막도록 다층구조·다중소속 모델링을 고려·구현하고 시간 흐름을 반영하는 모델도 포함한다. 가중치는 임의로 정하지 말고 수리과학·Psychometrics에서 추정된 값, 특히 fast-mlsirm이나 TEPP처럼 논문 근거가 있는 모형을 사용한다. 어떠한 휴리스틱과 Rule of thumbs도 금지하며, 근거 미확정 상태로 방치하지 말고 ContextualWisdomLab의 추론 엔진을 최대한 활용하고 SOLID 원칙을 지킨다. Deprecation Warning은 Suppression하지 말고 근본 문제를 해결한다. 합성 데모 데이터는 Unit test에는 쓸 수 있으나 Production에 반영하지 않는다. Python 웹 서버는 Multithreading을 지원하고 GIL이 문제면 Python 3.14를 사용한다. +> 구현 언어·연산·측정 원칙 Docstring·Test·Edge Case Coverage는 각 100%이고 초보자도 이해하게 쓴다. 수리과학·Psychometrics·EDA·데이터과학 core와 성능·안정성·보안 runtime은 Rust가 기본이다. Vector·Linear/Matrix Algebra·token size·GPU·CPU multithreading을 포함한다. Python은 비선호이며 LLM 편의·관성으로 고르지 않는다. 검증된 ML runtime이 Python 전용이고 Rust 대안이 기능·정확성·지원성을 못 맞출 때만 그 부분에 쓴다. 경계·근거·제거 조건을 ADR에 남기고 hot path는 Rust로 둔다. 확률표집은 설계·오차 목표·실패 분모를 명시한다. Atomistic fallacy 방지를 위해 다층·다중소속·시간을 모델링한다. 가중치는 fast-mlsirm·TEPP 등 논문 근거 모형에서 추정한다. 휴리스틱을 금지하고 미확정 근거는 추론 엔진과 SOLID로 해결한다. Deprecation Warning은 근본 해결한다. 합성 data는 Unit test에만 쓴다. 불가피한 Python web server는 multithreading을 지원하고 GIL 문제는 Python 3.14 또는 Rust로 푼다. ## 7. Realistic verification, load, and container testing -> 현실성 있는 검증과 부하·컨테이너 테스트는 제품 특성에 맞는 현실 사례와 정확성 기준을 포함한다. Psychometrics는 true parameter 대비 estimation RMSE와 true parameter 추정 재현성을 검증하고, 음악 분석은 실제 음원이 기대 분석값을 내는지 확인한다. 웹을 지원하면 Asynchronous 처리를 구현해 무응답을 방지하고 k6 end-to-end load test로 동시 접속 능력과 병목을 측정·개선한다. close_connection을 인스턴스 속성으로만 가정하는 잠재 버그를 점검한다. Docker는 Podman 또는 colima로 대체할 수 있다. 컨테이너 병목이면 shm_size와 PostgreSQL 등 응용 설정을 하드웨어에 맞게 자동 튜닝한다. 주로 compose로 운영해 k8s 전환성을 확보한다. Docker container 프로젝트명은 고정하되 테스트 격리 때만 override하고 달성 후 격리 컨테이너를 제거한다. MLX·CPU·CUDA·OpenCL의 Docker/Podman/Colima 처리법을 ADR에 기록·반영하고 Native Module 분리가 필요하면 독립 서비스로 개발한다. +> 현실성 있는 검증과 부하·컨테이너 테스트는 현실 사례와 제품별 정확성 기준을 포함한다. Psychometrics는 true parameter 대비 estimation RMSE·추정 재현성을, 음악 분석은 실제 음원의 기대 분석값을 검증한다. 웹은 비동기 처리·k6 E2E를 적용하고 모든 페이지 p95≤20ms를 요구한다. 초과하면 알고리즘·query·I/O·rendering을 profile하고 runtime·언어·framework가 원인이면 계약·정확성을 보존해 Rust 우선 기술·hot path·개발 언어를 바꾼다. 표본 축소·측정 제외·비현실적 cache warm-up을 금지한다. JavaScript bundle·heap·DOM·hydration·main thread·GC가 메모리·지연을 키우면 dependency·Frontend stack을 교체한다. close_connection도 점검한다. Docker는 Podman·colima로 대체 가능하다. 병목이면 shm_size·PostgreSQL을 hardware에 맞춰 튜닝한다. compose로 k8s 전환성을 지키고 프로젝트명은 test 격리 때만 override한다. MLX·CPU·CUDA·OpenCL 처리법을 ADR에 반영하고 Native Module은 필요시 독립 service로 분리한다. ## 8. LLM, orchestration, and embedding -> LLM·오케스트레이션·Embedding LLM이 필요한 테스트는 contextual-orchestrator 기반 OpenCode Agent로 만든다. contextual-orchestrator는 GitHub Secrets의 BYTEZ_API_KEY, NVIDIA_NIM_API_KEY, NVIDIA_NIM_API_KEY_SUB, OPENROUTER_API_KEY, OPENAI_API_KEY를 모두 써 auto model discovery로 최적 모형을 제공한다. embedding·responses·completions, audio, video, image, ommi-modal 등 가용 모델을 폭넓게 지원한다. 가능하면 반입해 쓰고 발견한 해당 저장소 문제도 함께 수정한다. LLM 사용 소프트웨어와 contextual-orchestrator는 Fugu·Conductor·TRINITY 연구를 근거로 단일 모델 라우팅과 심층 다중 Agent 오케스트레이션 사이의 계산량을 배분한다. 워크플로 단계, 재귀 깊이, 작업 분해, 접근 목록으로 test-time compute를 조절하고 역할별 reasoning effort를 다르게 하며 추론 수준 ablation을 수행한다. 속도는 핵심 고려사항이 아니며 정확성을 우선한다. 중앙 OpenCode, Strix, Noema는 모델당 두 시간 이상 걸릴 수 있음을 수용한다. LLM Chat model은 chat completion API와 responses API를 모두 지원하고 json_object와 json_schema를 모두 처리한다. Embedding은 문단·구문·DOM·송수신자 등 의미 단위를 식별해 chunking한다. 본문에 base64 이미지가 있으면 텍스트 인식, 객체 인식, 태그 설명, 이미지 별도 검색 방법을 연구 근거와 함께 DB 설계에 넣고 원래 삽입 위치를 보존해 그림 맥락까지 검색·표현한다. GitHub Actions scheduler는 contextual-orchestrator 기반 OpenCode Agent로 전환한다. COPILOT_GITHUB_TOKEN은 쓰지 않고 기존 리뷰 Agent 키 체계를 유지한다. - -**Note (flagged by CodeRabbit on this PR, 2026-08-30):** section 8's quoted text describes `contextual-orchestrator`'s general product capability — broad model/modality support and all-five-secret auto model discovery as a *design principle for the orchestrator itself*. It does not specify, and must not be read as overriding, which pool each CI consumer routes through: that is governed exclusively by `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md` and its doctoring records — `OpenCode` and `Noema` use the fail-closed, ZDR-prioritized `orchestrator/free` pool; only `Strix` security analysis uses the provider-diverse `orchestrator/auto` pool; private/internal review targets require an attested ZDR-only catalog and never fall back to a non-ZDR provider. Do not loosen any CI consumer's pool or credential scope on the strength of this section's general wording alone. - -**Note (2026-08-30, superseded by the merged pin flip — see the correction below):** an earlier draft of this note said Strix stayed on `orchestrator/auto` pending `free_family_diversity` reaching `>= 2`. That is no longer true and must not be read as current: `.github/workflows/strix.yml` now hardcodes `STRIX_MODEL`/`CONTEXTUAL_ORCHESTRATOR_POOL` to `orchestrator/free` and fails closed on any other value. This note originally went on to say that ADR-0003's 2026-08-30 amendment "records the owner's decision to accept the residual single-outage-domain risk immediately rather than wait for the evidence-gated threshold this note originally described" — that framing was false, as ADR-0003's own 2026-08-31 correction now records: no owner reviewed or accepted this switch or its risk. `free_account_diversity` (`scripts/ci/contextual_orchestrator_review_policy.py`; renamed from `free_family_diversity` once every KV credential became an independent discovery account rather than being grouped into a vendor "family", see #1468) remains useful as ongoing monitoring evidence for that open, unreviewed risk, not as a gate blocking the pin. - -## 9. Reference libraries, tool invocations, and ecosystem repositories - -> 참고 라이브러리와 호출 @Superpowers @GitHub @Figma @Visualize @Context7 @Product Design @Consensus를 활용한다. - -- **TEPP** — https://github.com/ContextualWisdomLab/TEPP — 다국어·시간·관계 측정용 Temporal Event Psychometrics Platform이며 통계·심리측정 산술은 Rust로 구현한다. -- **contextual-orchestrator** — https://github.com/ContextualWisdomLab/contextual-orchestrator — 논문 근거의 contextual model orchestration lab·enterprise admin design. -- **fast-mlsirm** — https://github.com/ContextualWisdomLab/fast-mlsirm — simple-structure MLSIRM/MLS2PLM은 Jeon, Jin, Schweinberger, and Baugh(2021), Kang and Jeon(2025), Molenaar and Jeon(2026)을 따른다. 인접 화면: Angoff delta-plot DIF(docs/delta_plot_dif.md), Bradley–Terry MM ranking(docs/bradley_terry_mm.md). 주요 인용·결정: docs/traceability/research-basis.md, docs/adr/README.md. 점수 해석·공정성은 AERA·APA·NCME(2014)를 따르며 이는 CWE/OWASP/NIST 통제가 아니다. -- **keyverse** — https://github.com/ContextualWisdomLab/keyverse — Keycloak 기반 독립 컴포넌트(Apache-2.0)이자 ContextualWisdom ecosystem 중앙 Identity Provider. -- **RankWeave** — https://github.com/ContextualWisdomLab/RankWeave — Python 3.10+용 무의존성·저장소 비종속 retrieval fusion/evaluation/statistical comparison/tuning/TREC benchmarking/auditable CLI workflow. -- **ThreadWeave** — https://github.com/ContextualWisdomLab/ThreadWeave — runtime dependency 없는 Python용 표준 기반 JWZ/RFC 5256 이메일 reference threading. -- **disksage** — https://github.com/ContextualWisdomLab/disksage — Windows/Linux/macOS 디스크 공간 관리자. 드라이브를 스캔하고 완전 오프라인 온디바이스 LLM이 삭제 안전성을 조언하며 OWL ontology로 파일을 정리한다. -- **wardnet** — https://github.com/ContextualWisdomLab/wardnet — ContextualWisdomLab Rust-first gateway·SOC control-plane baseline. -- **LineageWeave** — https://github.com/ContextualWisdomLab/LineageWeave — 명시적 선후행 링크 없는 짧은 timestamped record에서 git-branch식 lineage DAG를 재구성해 평면 자료를 탐색 가능한 branching thread로 바꾼다. 수리 연산은 소관이 아니므로 다른 라이브러리로 이관한다. +> LLM·오케스트레이션·Embedding LLM 작업은 contextual-orchestrator 기반 Agent로 만든다. `orchestrator/free` 고정. BYTEZ_API_KEY·NVIDIA_NIM_API_KEY·NVIDIA_NIM_API_KEY_SUB·OPENROUTER_API_KEY·OPENAI_API_KEY로 auto discovery해 embedding·responses·completions·audio·video·image·omni-modal을 지원한다. 소스·adapter는 복사하지 않고 released API·client·schema로 연결한다. .github reusable workflow와 얇은 owner·consumer caller로 통합 CI를 구성한다. PR·release·consumer 변경마다 exact SHA로 build·contract·API/schema·E2E·fallback·streaming·structured output·timeout·security·SBOM·provenance를 검증한다. 결함은 owner에서 RED→fix→GREEN→release한 뒤 consumer version을 올린다. mutable sibling head·branch URL·cross-repo path·workflow 복제를 금지한다. 임시 bridge는 owner issue·만료·삭제 조건을 ADR·CI에 둔다. Provider group명은 하드코딩하지 않는다. 별칭일 뿐이며 modality·context·reasoning·tool·structured output·streaming·가격·지연·가용성·정확도 등 검증된 특성으로 선택·fallback한다. Model timeout은 공통 상한 없이 기본값을 무제한(null)로 둔다. 통신 장애는 upstream provider timeout·오류로 끝난다. 관리자 Web에서 모델별 조회·설정·해제·복원, 단위·우선순위·상속·검증·감사·API를 제공하고 설정된 모델만 제한한다. reasoning·streaming·tool call을 경과시간만으로 끊지 않으며 사용자 취소·provider 종료·관리자 timeout을 구분한다. Fugu·Conductor·TRINITY 근거로 단일·다중 Agent의 test-time compute를 단계·재귀·분해·접근·역할별 effort로 배분·ablation한다. 정확성을 우선하고 OpenCode·Strix·Noema의 모델당 2시간 이상을 수용한다. Chat은 completions·responses와 json_object·json_schema를 지원한다. Embedding은 의미 단위로 나누며 base64 이미지 인식·검색·삽입 위치·맥락을 보존한다. + +**Note (2026-09-02, supersedes the 2026-08-30 note below):** this revision states the pool pin +directly in the quoted text ("`orchestrator/free` 고정"), removing the ambiguity the original wording +had. This matches the current implemented state: `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md`'s +2026-08-30 amendment already moved Strix from `orchestrator/auto` to `orchestrator/free`, and all four +central workflows that route through `contextual-orchestrator` (`opencode-review-dispatch.yml`, +`noema-review.yml`, `strix.yml`, `pr-review-autofix.yml`) hardcode `orchestrator/free` with fail-closed +validation as of this revision (verified directly against the workflow files, 2026-09-02). No further +pool-routing change is required by this section; ADR-0003 remains the authoritative record for *why*. + +**Note (2026-08-30, on the original wording, retained for history):** the original version of this +section described `contextual-orchestrator`'s general auto-discovery capability across all five +provider secrets as a product-level design principle, which — read in isolation — an agent could +mistake for license to loosen which pool a CI consumer routes through. That ambiguity no longer applies +now that this section states the pin explicitly, but the underlying rule stands: pool/credential-scope +routing for CI consumers is governed exclusively by ADR-0003, never loosened on the strength of this +section's general wording about the orchestrator's own capabilities. + +## 9. Core foundation and development/consumption boundaries + +> Core foundation과 개발·사용 경계 @Superpowers @GitHub @Figma @Visualize @Context7 @Product Design @Consensus를 쓴다. 매 실행 README·PRD·ARCHITECTURE·release에서 책임을 확인한다. core는 완성도가 아닌 반복 수요·권위·재사용 경계로 정하고 미완성이면 owner에서 완성·release한다. transient head는 production에 쓰지 않는다. +> * .github — workflow·review/security/release owner이며 ruleset·얇은 workflow_call로만 쓴다. +> * enterprise-architecture-core·context-graph-contracts — 전사 결정·versioned context 계약 원장이며 runtime·제품 DB는 제외한다. +> * ConceptWeave·semantic-data-portal — ontology 생성·publish와 catalog·governance·소비를 분담한다. +> * contextual-orchestrator·noema — 모델 orchestration과 GitHub OIDC 단기 권한·exact-head evidence를 분담한다. +> * keyverse — identity 원장. 제품은 OIDC/OAuth·SCIM·자체 form을 쓰고 table은 복제하지 않는다. +> * EgressWeave·OriginWeave·pingora-gateway·quarantine-sandbox-runtime — outbound·browser·edge·격리 core이며 부족한 기능은 owner에서 완성한다. +> * pg-llm-batch·EmbedRelay — batch/token과 embedding identity·vector migration owner다. +> * fast-mlsirm·TEPP — IRT/MLSIRM과 다국어·시간·event·relation 측정 owner이며 kernel 재구현을 금지한다. +> * RankWeave·ThreadWeave — retrieval fusion/evaluation/TREC와 JWZ/RFC 5256 threading owner다. +> * inkspan·DiagramWeave — editor/serialization과 diagram patch/render/CLI/LSP package다. +> * mhtml-etl-gateway — MHTML 검사·schema proposal·load·lineage owner다. +> * appguardrail·wardnet — SAST/SARIF와 Rust gateway/SOC baseline owner이며 범위를 과장하지 않는다. naruon·LineageWeave·psychometrics-commons·disksage·PolicyWeave·CalendarWeave·supply-chain-control-plane은 완성도가 아니라 domain product/composition consumer라 분류한다. 공통 기능은 core owner로 추출해 통합 CI로 개발한다. + +**Verification (2026-09-02):** every repository named above was confirmed to exist under +`ContextualWisdomLab` with exact-matching case via a direct GitHub repository listing on the date of +this revision: `.github`, `enterprise-architecture-core`, `context-graph-contracts`, `ConceptWeave`, +`semantic-data-portal`, `contextual-orchestrator`, `noema`, `keyverse`, `EgressWeave`, `OriginWeave`, +`pingora-gateway`, `quarantine-sandbox-runtime`, `pg-llm-batch`, `EmbedRelay`, `fast-mlsirm`, `TEPP`, +`RankWeave`, `ThreadWeave`, `inkspan`, `DiagramWeave`, `mhtml-etl-gateway`, `appguardrail`, `wardnet`, +`naruon`, `LineageWeave`, `psychometrics-commons`, `disksage`, `PolicyWeave`, `CalendarWeave`, +`supply-chain-control-plane`. None needed a spelling or case correction. + +**Open reconciliation item (2026-09-02, not yet resolved — do not silently pick one):** +`docs/CWL-MASTER-CONTEXT.md` §3 ("Ecosystem components") already documents roles for a subset of these +repos (`contextual-orchestrator`, `pg-llm-batch`, `fast-mlsirm`, `semantic-data-portal`, `noema`, +`appguardrail`, `inkspan`, `keyverse`, `wardnet`, among others) with narrower or differently-framed +descriptions than this section, and does not yet mention most of the repos newly named here +(`ConceptWeave`, `enterprise-architecture-core`, `context-graph-contracts`, `EgressWeave`, +`OriginWeave`, `pingora-gateway`, `quarantine-sandbox-runtime`, `EmbedRelay`, `DiagramWeave`, +`mhtml-etl-gateway`, `PolicyWeave`, `CalendarWeave`, `supply-chain-control-plane`, +`psychometrics-commons`, `LineageWeave`) at all. One specific tension worth flagging rather than +silently resolving: §3 currently describes `noema` as owning "the lightweight quarantine sandbox", +while this section groups `quarantine-sandbox-runtime` as its own dedicated repo alongside +`EgressWeave`/`OriginWeave`/`pingora-gateway` under "outbound·browser·edge·격리 core" — whether the +sandbox responsibility moved to a dedicated repo, or the two repos share it, has not been verified +against either repo's actual current content as of this revision. A future pass should read both +repos' READMEs/ARCHITECTURE docs and either update §3 to match this section, update this section if §3 +is the one that's current, or record an ADR if the split is a genuinely new decision — not guess. ## How to point a `/goal` session at this directive @@ -88,9 +155,29 @@ Because `/goal` truncates at 4000 characters, do not paste the sections above in short pointer, e.g. (Korean, ~260 chars, well under the cap): ```text -/goal ContextualWisdomLab/.github의 docs/product-goal-directive.md 전문을 지침으로 삼아 실행하라. 열린 PR마다 리뷰 확인→수정→Checks 재검증→병합→다음 개발을 중간 보고 없이 반복하고, PR·Issue 소진 후에도 Gap 기반 개발을 계속한다. 이 문서의 9개 절 전체(실행 루프, 동시작업/근본수정, 연구추적성, UX/UI, 아키텍처/DB, 언어/측정, 검증/부하, LLM/오케스트레이션, 참고 라이브러리)를 매 사이클 적용 대상으로 취급하고, 이 문서와 docs/CWL-MASTER-CONTEXT.md §7이 상충하면 상충을 해소하고 두 문서를 함께 갱신하라. 한 시간 간격으로 재예약하라. +/goal ContextualWisdomLab/.github의 docs/product-goal-directive.md 전문을 지침으로 삼아 실행하라. 열린 PR마다 리뷰 확인→수정→Checks 재검증→병합→다음 개발을 중간 보고 없이 반복하고, PR·Issue 소진 후에도 Gap 기반 개발을 계속한다. 이 문서의 9개 절 전체(실행 루프, 동시작업/근본수정, 연구추적성, UX/UI/i18n, 아키텍처/온톨로지/DB, 언어/측정, 검증/부하, LLM/오케스트레이션, Core foundation과 개발/사용 경계)를 매 사이클 적용 대상으로 취급하고, 이 문서와 docs/CWL-MASTER-CONTEXT.md가 상충하면 상충을 해소하고 두 문서를 함께 갱신하라. 매시간 재예약하라. ``` When this directive itself changes (the user revises a section, or an agent finds it conflicts with `docs/CWL-MASTER-CONTEXT.md` or a merged PR), edit this file in place and note the change in `docs/doctoring/` per the repo's traceability convention — do not fork a second copy elsewhere. + +## Revision history + +- **2026-08-30** — original nine-section directive recorded verbatim. Two reconciliation notes added + against `docs/CWL-MASTER-CONTEXT.md` (the wardnet naming-history conflict in §5, the + `orchestrator/auto`-vs-`orchestrator/free` pool-routing ambiguity in §8); see + `docs/doctoring/product-goal-directive.md` for the full PR #1429 review-finding trail. +- **2026-09-02** — the owner reissued the directive with all nine sections substantially expanded: + §2 added an explicit "build the immature core, don't bypass it" policy (RED-test-driven development + at the owner repo, ADR-gated exceptions only); §4 added detailed i18n requirements (8 supported + languages, DB-backed versioned translation resources, no full-catalog client bundles) and clarified + Keycloak's role (auth backend only, product-owned login/signup/recovery forms); §5 broadened the + naming rule from DB objects to all code identifiers and added an ontology-ownership split across four + named repos; §8 folded the `orchestrator/free` pin directly into the quoted text, resolving the prior + ambiguity; §9 replaced the general reference-library list with a full named core-repo table and an + explicit core-vs-domain-product classification. Recorded verbatim, superseding the 2026-08-30 text of + every section; both the 2026-08-30 and 2026-09-02 reconciliation notes are kept inline above (each + marked with its date) so neither the original context nor the correction is lost. See + `docs/doctoring/product-goal-directive.md` for this revision's own reconciliation record, including + the open §3-vs-§9 repo-role item noted under §9 above. From 707910f124e562b33d516e41b63285b08c45810e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 07:57:41 +0000 Subject: [PATCH 2/7] docs: layer the third same-day product-goal-directive revision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner issued one further refinement of the directive after the second revision (already on this branch/PR): §1 clarifies "0 open PRs" must come from merges or full successor-absorption, never a bare close; §2 gains a substantial repair-vs-close PR-lifecycle policy; §4 drops the named-framework shortlist for framework-agnostic criteria; §5 moves its per-repo ontology breakdown into §9; §8 spells out exactly how the orchestrator/free pin must be implemented at the workflow layer; §9 adds a definition of what "core foundation" means and reorganizes the repo table into five categories with concrete interim-boundary mechanisms. Continuing the existing branch/PR per the directive's own "edit this file in place, do not fork a second copy" policy, applied to the PR level too, since #1692 was still open when this third revision arrived. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN --- docs/doctoring/product-goal-directive.md | 106 +++++++++ docs/product-goal-directive.md | 271 ++++++++++++++--------- 2 files changed, 278 insertions(+), 99 deletions(-) diff --git a/docs/doctoring/product-goal-directive.md b/docs/doctoring/product-goal-directive.md index 189c5aa33c..b3bee6c279 100644 --- a/docs/doctoring/product-goal-directive.md +++ b/docs/doctoring/product-goal-directive.md @@ -249,3 +249,109 @@ fixed: - `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md` — the authoritative ADR for *why* the pool pin is `orchestrator/free`; this revision's §8 note points here rather than restating the reasoning. + +## 2026-09-02 revision (third, same-day refinement) + +- **Subject:** the owner issued a third, further-refined version of the + directive the same day as the second revision above, delivered directly in + an agent session (again not via a PR comment). Per this file's own conflict + policy, `docs/product-goal-directive.md` was updated in place again — on + the same branch/PR as the second revision (`docs/product-goal-directive-2026-09-02-revision`, + ContextualWisdomLab/.github#1692), not a new PR, since that PR was still + open/draft and unmerged when this third revision arrived. The prior + agent-session identity that authored the second revision's PR body could + not be directly confirmed as the same session continuing (concurrent + sessions under the same account are normal per §2's "don't assume + concurrent commits are a race" instruction), so this revision's notes are + additive to, not a rewrite of, the second revision's verification record + above — both are kept so the reconciliation trail stays intact regardless + of which session's account of events is read first. + +### What changed in the directive text (second revision → third revision) + +- §1: added an explicit constraint on what "0 open PRs" may mean — reaching + it via merge or via a successor PR's full absorption of a predecessor's + valid delta only, never via a bare close. This is a forward reference to + §2's new repair-vs-close policy below. +- §2: gained a substantial, entirely new PR-lifecycle policy not present in + the second revision at all — a `single-writer`/DDD violation, wrong + base/merge conflict, ADR-number collision, premature `Accepted` status, an + unprotected dependency, or a missing test/fixture/contract is a **repair + finding**, not grounds for closing a PR. Required response: downgrade to + Draft/Proposed and non-force restack/retarget onto the correct owner + stack (never force-push over someone else's branch); resolve a + `single-writer` conflict by integrating both deltas, never discarding + either; when an agent can't fix a PR directly, a successor must fully + absorb its valid delta and continue the predecessor's intent; a PR + blocked on an unlanded prerequisite stays open while the prerequisite is + completed; a mistakenly closed PR is recovered via reopen or successor, + not left closed. Closing a PR at all is permitted only for an explicit + user request, no valid delta remaining, a malicious change, or full + successor absorption — and even then, "closed" is a label, not + confirmation the underlying work landed anywhere. +- §4: dropped the second revision's named-framework shortlist + ("React·Vite·shadcn/ui·jQuery 4"); the stack-choice criteria are now + stated framework-agnostically (security, maintainability, standards, + accessibility, measured performance), while still naming `shadcn/ui` + explicitly as the owned component source. +- §5: moved the second revision's detailed per-repo ontology-owner + breakdown (which repo does discover/propose/align/validate/publish, etc.) + out of §5 and into §9, where the full core-repo table now lives; §5 itself + now just states the responsibilities are split across separate owners and + points to §9. +- §8: spelled out, in the quoted text itself, exactly how the + `orchestrator/free` pin must be implemented at the GitHub Actions layer — + workflows select only the pool id and a gateway token, never a concrete + provider/model/group name or a paid-fallback flag; all free-candidate + discovery/routing/fallback happens inside contextual-orchestrator itself; + a workflow fails closed (not to a paid model) when no free capability is + available. This is strictly more specific than the second revision's bare + "`orchestrator/free` 고정," not a change in intent. +- §9: added an explicit definition of what "core foundation" means (not a + common install for every product; a selective control-plane/service/ + library one repo canonically owns because a responsibility repeats across + products, with role/maturity verified from protected-branch evidence, and + an open PR proposing core status is `Proposed`, not authoritative yet); + reorganized the same 21-repo list into five named categories (조직·계약 / + 의미·데이터 / AI·운영 / Identity·보안·runtime / 재사용 기능) instead of the + second revision's flat bullet list; and added concrete interim-boundary + mechanisms for the §2 build-the-core policy — while an owner is still + building a needed capability, a consumer holds the boundary with a port, + an ACL, a feature flag, or a test double, and must not read the owner's + source, database, or a temporary branch directly. + +### Verification performed this revision + +1. **Repo list unchanged, re-confirmed.** The third revision's §9 names the + same 21 core repos as the second revision (reorganized into categories, + not expanded or reduced), plus the same second-revision consumer set is + implied by §9's new opening definition even though the third revision's + quoted text doesn't restate the seven-repo consumer list verbatim. No new + repo-existence check was needed beyond the second revision's — recorded + inline in the directive (§9 "Verification" note) rather than re-running + the same live-listing check for an unchanged set. +2. **§2 repair-vs-close policy — no conflict found against + `docs/CWL-MASTER-CONTEXT.md` or `docs/agent-github-project-protocol.md`.** + This is new operational policy (how to handle a defective PR) rather than + a restatement of an existing binding convention, so there was nothing to + reconcile it against; it does not contradict anything already binding. +3. **§8's newly-explicit workflow-implementation language — re-verified + against the same four workflow files as the second revision** + (`opencode-review-dispatch.yml`, `noema-review.yml`, `strix.yml`, + `pr-review-autofix.yml`): all four already set only the pool id plus a + gateway token, never a provider/model/group name or paid-fallback flag, + and `strix.yml` fails closed on any other requested value — the third + revision's more detailed wording matches confirmed reality exactly, same + conclusion as the second revision's check, now more specifically stated. + +### Audit trail (2026-09-02, third revision) + +- `docs/product-goal-directive.md` — updated again in place; each section + with third-revision changes carries its own dated note distinguishing + second-revision text from third-revision text, and the "Revision history" + section at the end of the file now has three dated entries. +- ContextualWisdomLab/.github#1692 (branch + `docs/product-goal-directive-2026-09-02-revision`) — the same open PR + carrying both the second and third revision's commits; not superseded by + a new PR, per this directive's own "edit this file in place... do not + fork a second copy elsewhere" policy applied to the PR-branch level too. diff --git a/docs/product-goal-directive.md b/docs/product-goal-directive.md index 5929b02b21..3d816f337a 100644 --- a/docs/product-goal-directive.md +++ b/docs/product-goal-directive.md @@ -1,9 +1,10 @@ # Product goal directive — autonomous PR/merge/development loop -**Status:** active standing directive · **Owner intent recorded:** 2026-08-30, revised 2026-09-02 · -**Scope:** the full ContextualWisdomLab ecosystem (every repo an agent can reach from this org, -leveraged in order of product responsibility / reuse boundary / docs / implementation / consumption — -not by name). +**Status:** active standing directive · **Owner intent recorded:** 2026-08-30, revised 2026-09-02 +(three successive refinements in one day; this file holds the final, most-refined text of each +section) · **Scope:** the full ContextualWisdomLab ecosystem (every repo an agent can reach from this +org, leveraged in order of product responsibility / reuse boundary / docs / implementation / +consumption — not by name). ## Why this file exists @@ -24,55 +25,100 @@ is wrong — do not silently pick one. The directive is recorded verbatim (Korean, as authored) in the nine sections below, each given a short English heading for navigability. Do not paraphrase or shorten these sections when copying them -elsewhere; link to this file instead. The 2026-09-02 revision replaced the full text of all nine -sections (the owner reissued the directive with substantially expanded detail in §2, §4, §5, §8, and -§9); see "Revision history" at the end of this file and `docs/doctoring/product-goal-directive.md` for -the reconciliation notes on both the original and this revision. +elsewhere; link to this file instead. The owner issued three successive revisions on 2026-09-02; each +section below holds the text of the **third and final** revision. See "Revision history" at the end of +this file and `docs/doctoring/product-goal-directive.md` for the reconciliation notes across all +revisions. ## 1. Execution goal and continuous loop -> 실행 목표와 지속 Loop 열린 PR마다 보고 없이 리뷰→수정→Checks 재검증→병합→다음 개발을 반복하라. PRD로 Loop·Goal을 조정해 PR을 병합하거나 0개로 만들며 상용화한다. 목표는 200억 달러 판매 품질과 고객 체감 Gap 해소다. ADR·리서치·현행 데이터·PR에서 PRD·TRD·UML·Gap·조치를 도출해 docs/product-technical-gap-baseline.md에 갱신하라. 매시간 예약 메시지를 개선한다. PR·Issues 소진 뒤에도 Gap 개발·병합과 ContextualWisdomLab 저장소·Connector 연계를 계속한다. 전 라이브러리 PRD와 조직·저장소명 대소문자를 지킨다. 리뷰·Checks 대기는 Blocker가 아니다. 실패를 즉시 수정·재실행하고 다른 안전한 일을 계속한다. 저장소는 책임·재사용 경계·구현·소비 관계로 고르고 ADR·Goal·Loop를 갱신한다. +> 실행 목표와 지속 Loop +> 열린 PR마다 리뷰→수정→Checks 재검증→병합→다음 개발을 반복하라. PRD로 Loop·Goal을 조정하되 PR 0개는 병합이나 검증된 successor의 유효 delta 완전 승계로만 만들고 단순 Close하지 않는다. 목표는 200억 달러 판매 품질과 고객 체감 Gap 해소다. ADR·현행 근거·PR에서 PRD·TRD·UML·Gap·조치를 도출해 docs/product-technical-gap-baseline.md를 갱신하라. +> 매시간 예약 메시지를 개선한다. PR·Issues 소진 뒤에도 Gap 개발·병합과 ContextualWisdomLab 저장소·Connector 연계를 계속하며 PRD와 명칭 대소문자를 지킨다. 리뷰·Checks 대기는 Blocker가 아니다. 실패를 즉시 고쳐 재실행하며 안전한 일을 계속한다. 저장소는 책임·재사용·구현·소비 경계로 고르고 ADR·Goal·Loop를 갱신한다. -## 2. Concurrent operation, PR handling, and root-cause fixes +**Note (2026-09-02, third revision):** this section now explicitly forbids reaching "0 open PRs" by +simply closing them — a PR only leaves the open set via merge, or via a successor PR that fully absorbs +its valid delta (see §2's repair-vs-close policy below, which this section's "0개" language now +defers to). Earlier automation or agents that treated "reduce open PR count" as license to close +stalled PRs should stop; §2 is now the controlling text for what "resolved" means. -> 동시 작업·PR 운영·근본 수정 동시 Commit·Push를 경합으로 단정하거나 Force Push·중단하지 말고 취지를 이어간다. Commit 전 병합·삭제 근거를 기록하고 목적을 마친 Self-modifying/Source-fix Workflow는 삭제한다. PR은 Stack해 merge-ready로 만들고 유관 원인도 처리한다. Stacked PR의 OpenCode 리뷰가 빠지면 .github를 고친다. Agent 간 대화·Spawn을 쓴다. 수동 해법은 코드·설정화한다. PYTHONPATH=.·Actions·runtime 오류는 로그 RCA로 제거한다. 내부 문제는 canonical owner·원시 공급자까지 고친다. 필요한 core가 미성숙해도 consumer에서 복제·우회·제외하지 말고 owner에 RED test·계약·기능·문서·release를 개발해 통합 CI GREEN 후 versioned release로 연결한다. 경계가 틀리거나 공통 수요가 없을 때만 ADR 근거로 제외한다. DietrichGebert/ponytail·obra/superpowers를 쓰되 "무조건 질문"은 무시한다. tirth8205/code-review-graph·colbymchenry/codegraph도 인덱싱한다. 한국어 문구·문서·번역은 https://github.com/epoko77-ai/im-not-ai로 의미·사실·수치·고유명사를 보존하며 윤문한다. +## 2. Concurrent operation, PR handling, and root-cause fixes -**Note (2026-09-02):** `epoko77-ai/im-not-ai` is a repository under a different GitHub account -(`epoko77-ai`), not `ContextualWisdomLab` — it is an external Korean-copyediting tool referenced by -URL, not an org-owned core repo, and is not part of the §9 core-repo list below. +> 동시 작업·PR 운영·근본 수정 +> 동시 Commit·Push를 경합으로 단정하거나 Force Push하지 말고 취지를 잇는다. Commit 전 병합·삭제 근거를 남기고 목적을 마친 Self-modifying/Source-fix Workflow는 제거한다. PR을 Stack해 merge-ready로 만들고 유관 원인도 고친다. Stacked PR 리뷰가 빠지면 .github를 수리하고 Agent 대화·Spawn을 쓴다. +> single-writer·DDD 위반, 잘못된 base·충돌, ADR 번호 충돌, 성급한 Accepted, 미보호 dependency, 누락된 test·fixture·contract는 Close가 아닌 repair finding이다. Draft·Proposed로 낮추고 owner stack에 non-force restack·retarget해 수리한다. single-writer는 delta 폐기가 아닌 통합이다. 직접 못 고치면 successor가 delta를 완전 승계하고 predecessor를 잇는다. foundation 미착지 시 prerequisite를 완성하며 PR은 유지한다. 오폐쇄는 reopen·successor로 복구한다. Close는 사용자 명시, 유효 delta 없음, 악성 변경, 완전 승계에만 허용하며 표시는 종결이 아니다. +> 수동 해법은 코드·설정화하고 PYTHONPATH=.·Actions·runtime 오류는 로그 RCA로 없앤다. 내부 결함은 canonical owner·원시 공급자에서 고쳐 통합 CI와 release까지 잇는다. 경계나 공통 수요가 없을 때만 ADR로 제외한다. +> ponytail·superpowers를 쓰되 "무조건 질문"은 무시하고 code-review-graph·codegraph를 인덱싱한다. 한국어 문구·문서·번역에는 https://github.com/epoko77-ai/im-not-ai를 적용하되 의미·사실·수치·고유명사를 보존한다. + +**Note (2026-09-02, third revision — new content, not in the second revision):** this revision adds a +substantial, previously-unstated PR-lifecycle policy: a `single-writer`/DDD violation, wrong base or +merge conflict, ADR-number collision, premature `Accepted` status, an unprotected dependency, or a +missing test/fixture/contract is a **repair finding**, never grounds for closing a PR outright. The +required response is to downgrade the PR to Draft/Proposed and non-force restack/retarget it onto the +correct owner stack — never force-push over someone else's branch to "fix" it. Where a `single-writer` +conflict exists, the resolution is **integration of both deltas**, not discarding either one. When an +agent cannot fix a PR directly, a successor PR must fully absorb its valid delta and continue the +predecessor's intent, not start over and drop what the predecessor already proved. If a PR is blocked on +an unmerged prerequisite (a foundation/core repo change hasn't landed yet), the correct action is to +complete that prerequisite — the blocked PR stays open meanwhile. A PR closed by mistake is recovered +via reopen or a successor, not left closed. Closing a PR at all is permitted only for: an explicit user +request, no valid delta remains, the change is malicious, or a successor has fully absorbed it — and +even then, marking a PR "closed" is not itself the goal; the underlying work still needs to land +somewhere. `epoko77-ai/im-not-ai` is a repository under a different GitHub account (`epoko77-ai`), not +`ContextualWisdomLab` — an external Korean-copyediting tool referenced by URL, not an org-owned core +repo, and not part of the §9 core-repo list below. ## 3. Research, standards, and documentation traceability -> 연구·표준·문서 추적성 최신 권위 표준·논문을 조사해 APA 7th로 인용하고 doctoring에 남긴다. Local Zotero API가 되면 기존 자료나 OA 논문을 보강한다. 근거는 늘 exact-head·PR·모듈·API에 연결하고 충돌을 고친다. AGENTS.md·CLAUDE.md·ARCHITECTURE.md·CHANGELOG.md·ADR, ERD·UML·PRD·TRD·user story·storyboard·wireframe·Storybook·security/test/operability baseline을 갱신한다. 릴리즈 가능하면 버전·CHANGELOG를 올려 배포하고 GitHub.io는 실제 출판한다. 의사결정은 맥락을 잊거나 처음 보는 사람도 문제·제약·대안·선택/기각 이유·근거·위험·효과·후속 조치를 재구성하도록 구체적이고 자세히 기록한다. 결론·전제를 생략하지 말고 사용자·운영·장애 장면이 떠오를 사례와 증거를 exact-head·로그·이슈·PR·ADR·실험에 연결해 다른 Agent가 검증·계속하게 한다. +> 연구·표준·문서 추적성 +> 권위 표준·논문을 조사해 APA 7th로 인용하고 doctoring에 남긴다. Local Zotero API가 되면 자료·OA 논문을 보강한다. 근거는 exact-head·PR·모듈·API에 연결하고 모순을 고친다. +> AGENTS.md·CLAUDE.md·ARCHITECTURE.md·CHANGELOG.md·ADR와 ERD·UML·PRD·TRD·UX·security/test/operability를 갱신한다. 가능하면 버전·CHANGELOG를 올려 배포하고 GitHub.io를 언급하면 실제 출판한다. +> 의사결정은 처음 보는 사람도 문제·제약·대안·선택/기각 이유·근거·위험·효과·후속 조치를 재구성하게 구체적이고 자세히 기록한다. 결론·전제를 생략하지 말고 사용자·운영·장애 장면이 보이는 사례와 증거를 exact-head·로그·이슈·PR·ADR·실험에 연결해 다른 Agent가 검증·계속하게 한다. ## 4. UX/UI, i18n, and customer-facing expression -> UX·UI·i18n과 고객 표현 Figma·Storybook·ui-ux-pro-max·Anti-Slop-UI를 쓴다. UI는 모두 재사용 객체이고 페이지는 조합으로 만든다. token·Figma ID를 ADR에 남긴다. Storybook에서 정상·로딩·빈·오류·권한·반응형·상호작용 상태를 격리 개발·문서화하고 스크린샷·E2E로 접근성·터치·성능·타이포그래피·색상·폼·탐색·차트를 감사한다. shadcn/ui는 component source로 Storybook과 대체 관계가 아니다. Frontend stack은 고정하지 않으며 React·Vite·shadcn/ui·jQuery 4 등은 보안·유지보수·표준·접근성·성능을 충족할 때 쓴다. 내부 경계를 숨기고 다음 행동을 안내한다. Keyverse는 인증 backend로 유지하되(Direct Grant/ROPC 또는 Keycloak REST API), 로그인·가입·복구는 제품 자체 form으로 만든다. token CSS·Action Edge·Interaction UX를 검증한다. i18n은 한국어·영어·일본어·중국어·베트남어·스페인어·독일어·프랑스어를 지원한다. UI 폭·줄바꿈·CJK·텍스트 팽창·font fallback·locale 형식을 고려하고 언어별 Storybook·E2E로 잘림·겹침·의미 축약을 막는다. 번역 원장은 파일·JS bundle이 아닌 DB의 versioned resource다. server/native가 화면 key만 조회·cache하며 browser에 전체 catalog·무거운 i18n JavaScript를 싣지 않고 SPA를 전제하지 않는다. 공통 관리 제품이 없으면 새 저장소를 만들어 제품별 번역·검토·승인·배포·rollback API·관리 UI를 제공한다. +> UX·UI·i18n과 고객 표현 +> Figma·Storybook·ui-ux-pro-max·Anti-Slop-UI를 쓴다. 모든 UI는 재사용 객체이며 페이지는 그 조합이다. token·Figma ID를 ADR에 남긴다. Storybook에서 정상·로딩·빈·오류·권한·반응형·상호작용 상태를 문서화하고 스크린샷·E2E로 ui-ux-pro-max 전 범주를 감사한다. shadcn/ui는 제품 소유 component source, Storybook은 검증 환경이다. Frontend stack은 보안·유지보수·표준·접근성·측정 성능으로 고른다. +> 내부 경계를 숨기고 다음 행동을 안내한다. Keyverse는 인증 backend로 유지하되(Direct Grant/ROPC 또는 Keycloak REST API), 로그인·가입·복구는 제품 자체 form으로 만든다. token CSS·Action Edge·Interaction UX를 검증한다. +> i18n은 한국어·영어·일본어·중국어·베트남어·스페인어·독일어·프랑스어를 지원한다. UI 크기·줄바꿈·CJK·텍스트 팽창·font fallback·locale을 고려하고 언어별 Storybook·E2E로 잘림·겹침을 막는다. 번역 원장은 파일·JS bundle이 아닌 DB versioned resource다. server/native는 화면 key만 조회·cache하며 전체 catalog·무거운 i18n JavaScript·SPA를 전제하지 않는다. 공통 관리 제품이 없으면 새 저장소에서 제품별 번역·검토·승인·배포·rollback API·관리 UI를 제공한다. + +**Note (2026-09-02, third revision):** this revision no longer names specific frontend frameworks +(the earlier revision listed "React·Vite·shadcn/ui·jQuery 4"); the stack-choice criteria (security, +maintainability, standards, accessibility, measured performance) are now framework-agnostic. Read this +as loosening the earlier revision's implicit framework shortlist, not as silence on the question — +`shadcn/ui` is still named explicitly as the owned component source. ## 5. Architecture, ontology, naming, and database conventions -> 아키텍처·온톨로지·명명·데이터베이스 DDD의 Subdomain·Bounded Context·Context Map·Ubiquitous Language와 Aggregate·Entity·Value Object·Domain Service·Repository·Domain Event·Invariant를 ADR·코드·API·DB·test에 일치시킨다. Aggregate는 최소 transaction 경계로 두고 외부·legacy는 ACL로 격리하며 Shared Kernel을 최소화한다. 모듈러 MSA를 지향하고 비대한 Monolith는 책임별로 분리하며 옛 이름을 고친다. 통합 온톨로지는 ConceptWeave가 observe→discover→propose→align→validate→review→publish와 semantic release를, semantic-data-portal이 catalog·governance·소비를, context-graph-contracts가 상호운용 계약을, enterprise-architecture-core가 Context Map·결정을 맡는다. domain truth·Ubiquitous Language는 제품 owner에 남긴다. 개념·관계·dimension·measure·mapping은 evidence·provenance·validity·confidence·status·deprecation·locale label을 가진 immutable release로 배포한다. consumer는 released API/contract·ACL만 쓰고 파일 복사·cross-service SQL·미승인 publication을 금지한다. UI 번역과 ontology label의 원장은 분리한다. 변수·상수·매개변수·필드·함수·메서드·클래스·타입·모듈·패키지·API·DB 객체·파일·디렉터리는 두 단어 이상 snake_case·camelCase·PascalCase로 명명하고 snake_case를 우선한다. 언어·framework·외부 계약 관례는 경계에서 변환하고 위반명은 치환한다. DB는 3NF·Hot Partition 대비·Lock·필요시 Read/Write 분리·항목별 UPSERT를 지킨다. placeholder Buyer는 실제 도메인명으로 바꾼다. CSAP·SOC 2를 고려한다. PII Masking이 업무를 마비시키면 준수형 비Masking 대안을 설계한다. 실데이터 인명·기관명은 익명화하고 PYPI API Key·Public 배포 전제를 반영한다. - -**Reconciliation (naming scope, 2026-09-02 — supersedes the 2026-08-30 note below for this point):** -this section's identifier-naming rule is now explicitly broader than the original directive's — it -covers every code identifier (variables, functions, classes, modules, files, directories, API and DB -object names), not DB objects alone, and says a violating name "gets replaced" (위반명은 치환한다). -Read together with `docs/CWL-MASTER-CONTEXT.md` §7's binding, narrower rule — *"DB object names = 2+ -word snake_case (don't rename existing Camel/Pascal)"* — the two do not contradict as long as this -broader rule is applied the same way: **going forward, on code an agent is already touching or -creating**, not as a mandate to sweep the ecosystem and force-rename every existing identifier that -doesn't fit. A repo-wide, blind rename of existing public APIs, classes, or DB objects is a -high-blast-radius, potentially breaking change for real consumers — exactly the kind of action this -org's own engineering conventions (minimal, reviewed, evidence-based changes; ADR-recorded reasoning) -guard against. §7's DB-object grandfather clause remains binding and unambiguous: existing Camel/Pascal -DB objects are not renamed on the strength of this section alone. Where a genuinely repo-wide rename is -warranted (a name is actively misleading or the repo/product itself was renamed), record it as an ADR -with its own migration plan, not as a blanket action under this directive. +> 아키텍처·온톨로지·명명·데이터베이스 +> DDD의 Subdomain·Bounded Context·Context Map·Ubiquitous Language(UL)와 Aggregate·Entity·Value Object·Domain Service·Repository·Event·Invariant를 ADR·코드·API·DB·test에 맞춘다. Aggregate는 최소 transaction 경계, 외부·legacy는 ACL로 격리하고 Shared Kernel은 최소화한다. 비대한 Monolith는 책임별 저장소로 나누고 옛 이름을 고친다. +> 통합 온톨로지의 생성·publish, catalog·소비, 상호운용 계약, EA 결정은 owner를 분리한다. 제품의 domain truth·UL은 옮기지 않는다. release는 evidence·provenance·유효기간·confidence·status·locale label을 가진다. consumer는 released contract·ACL만 사용하며 파일 복사·cross-service SQL·미승인 publication을 금지한다. UI 번역과 ontology label 원장은 분리한다. +> 변수·상수·인자·필드·함수·메서드·클래스·타입·모듈·패키지·API·DB 객체·파일·디렉터리는 두 단어 이상 snake_case·camelCase·PascalCase로 명명하고 snake_case를 우선한다. 언어·framework·외부 계약 관례는 경계에서 변환하며 위반명은 치환한다. DB는 3NF·Hot Partition 대비·Lock·필요시 Read/Write 분리·항목별 UPSERT를 지킨다. placeholder Buyer는 실제 도메인명으로 바꾼다. +> CSAP·SOC 2를 고려한다. PII Masking이 업무를 마비시키면 준수형 비Masking 대안을 설계한다. 실데이터 인명·기관명은 익명화하고 PYPI API Key·Public 배포 전제를 반영한다. + +**Note (2026-09-02, third revision):** the detailed per-repo ontology-owner breakdown that the second +revision carried in this section (which repo does discover/propose/align/validate/publish, etc.) has +moved to §9 below, where the full core-repo table now lives — this section states only that the +responsibilities are split across separate owners, and points there for who owns what. + +**Reconciliation (naming scope, 2026-09-02, still applicable):** this section's identifier-naming rule +spans every code identifier (variables through directories), not just DB objects, and says a violating +name "gets substituted" (위반명은 치환한다) with no carve-out in the quoted text — unchanged across all +three revisions of this section. `docs/CWL-MASTER-CONTEXT.md` §7's binding convention — *"DB object +names = 2+ word snake_case (don't rename existing Camel/Pascal)"* — still governs DB objects +specifically and still grandfathers existing Camel/Pascal DB objects; nothing in any revision repeals +that clause. Read this section's broader identifier rule as prospective and opportunistic: apply it to +new code, and fix existing violations incrementally when touching that code for another reason — not as +a mandate to sweep every existing repo renaming public APIs, exported classes, or shipped DB objects in +one pass. A rename that breaks a published contract (a public API signature, a released client, an +external integration) is exactly the kind of consequential, hard-to-reverse action this org's own +engineering conventions ask an agent to pause and confirm before taking, not something "위반명은 +치환한다" authorizes unilaterally at scale. **Note (2026-08-30, on the original wording, retained for history):** the original (2026-08-30) version of this section named "wardnet" as an example of an "old name" (옛 이름) to rename *away from*. Per `docs/CWL-MASTER-CONTEXT.md` §3/§10, `waf-ids-ai-soc` → **wardnet** is an already-completed -rename — wardnet is the current canonical product name. The 2026-09-02 revision above no longer +rename — wardnet is the current canonical product name. Neither the second nor the third revision contains that specific wording, and §9 below lists wardnet as a current core-repo owner, consistent with treating it as the canonical name. If a future revision of this directive reintroduces "wardnet" as an old-name example, treat that as the same error this note originally corrected, not as new intent @@ -80,74 +126,95 @@ to rename the product. ## 6. Implementation language, computation, and measurement principles -> 구현 언어·연산·측정 원칙 Docstring·Test·Edge Case Coverage는 각 100%이고 초보자도 이해하게 쓴다. 수리과학·Psychometrics·EDA·데이터과학 core와 성능·안정성·보안 runtime은 Rust가 기본이다. Vector·Linear/Matrix Algebra·token size·GPU·CPU multithreading을 포함한다. Python은 비선호이며 LLM 편의·관성으로 고르지 않는다. 검증된 ML runtime이 Python 전용이고 Rust 대안이 기능·정확성·지원성을 못 맞출 때만 그 부분에 쓴다. 경계·근거·제거 조건을 ADR에 남기고 hot path는 Rust로 둔다. 확률표집은 설계·오차 목표·실패 분모를 명시한다. Atomistic fallacy 방지를 위해 다층·다중소속·시간을 모델링한다. 가중치는 fast-mlsirm·TEPP 등 논문 근거 모형에서 추정한다. 휴리스틱을 금지하고 미확정 근거는 추론 엔진과 SOLID로 해결한다. Deprecation Warning은 근본 해결한다. 합성 data는 Unit test에만 쓴다. 불가피한 Python web server는 multithreading을 지원하고 GIL 문제는 Python 3.14 또는 Rust로 푼다. +> 구현 언어·연산·측정 원칙 +> Docstring·Test·Edge Case Coverage는 각 100%다. 수리과학·Psychometrics·EDA·데이터과학 core와 성능·보안 runtime은 Rust로 만들며 Vector·Linear/Matrix Algebra·token size·GPU·CPU multithreading을 포함한다. Python은 비선호며 LLM 편의로 고르지 않는다. Python 전용 ML runtime에 실용적 Rust 대안이 없을 때만 그 부분에 쓰며 범위·근거·제거 조건을 ADR에 남기고 hot path는 Rust로 둔다. +> 확률표집은 설계·오차 목표·실패 분모를 명시하고 다층·다중소속·시간 모델로 Atomistic fallacy를 막는다. 가중치는 fast-mlsirm·TEPP 등 논문 근거로 추정하며 휴리스틱을 금지한다. 미확정 근거는 추론 엔진·SOLID로 해결한다. Deprecation Warning은 근본 해결하고 합성 data는 Unit test에만 쓴다. Python web server는 multithread이며 GIL 병목은 3.14나 Rust로 푼다. ## 7. Realistic verification, load, and container testing -> 현실성 있는 검증과 부하·컨테이너 테스트는 현실 사례와 제품별 정확성 기준을 포함한다. Psychometrics는 true parameter 대비 estimation RMSE·추정 재현성을, 음악 분석은 실제 음원의 기대 분석값을 검증한다. 웹은 비동기 처리·k6 E2E를 적용하고 모든 페이지 p95≤20ms를 요구한다. 초과하면 알고리즘·query·I/O·rendering을 profile하고 runtime·언어·framework가 원인이면 계약·정확성을 보존해 Rust 우선 기술·hot path·개발 언어를 바꾼다. 표본 축소·측정 제외·비현실적 cache warm-up을 금지한다. JavaScript bundle·heap·DOM·hydration·main thread·GC가 메모리·지연을 키우면 dependency·Frontend stack을 교체한다. close_connection도 점검한다. Docker는 Podman·colima로 대체 가능하다. 병목이면 shm_size·PostgreSQL을 hardware에 맞춰 튜닝한다. compose로 k8s 전환성을 지키고 프로젝트명은 test 격리 때만 override한다. MLX·CPU·CUDA·OpenCL 처리법을 ADR에 반영하고 Native Module은 필요시 독립 service로 분리한다. +> 현실성 있는 검증과 부하·컨테이너 +> 테스트는 현실 사례와 제품별 정확성 기준을 쓴다. Psychometrics는 true parameter 대비 RMSE·재현성을, 음악은 실제 음원의 기대값을 검증한다. +> 웹은 비동기 처리·k6 E2E를 적용해 모든 페이지 p95≤20ms를 맞춘다. 초과하면 profile하고 runtime·언어·framework가 원인이면 계약·정확성을 보존해 Rust 우선 기술·hot path·언어로 바꾼다. 표본 축소·측정 제외·비현실적 cache warm-up은 금지한다. JavaScript bundle·heap·DOM·hydration·main thread·GC가 메모리·지연을 키우면 dependency·rendering·Frontend stack을 교체한다. close_connection도 점검한다. +> Docker는 Podman·colima로 대체 가능하다. 병목이면 shm_size·PostgreSQL을 장비에 맞춰 튜닝한다. compose로 k8s 전환성을 지키고 프로젝트명은 test 격리 때만 바꾼다. MLX·CPU·CUDA·OpenCL 처리법을 ADR에 남기고 Native Module은 필요시 독립 service로 분리한다. ## 8. LLM, orchestration, and embedding -> LLM·오케스트레이션·Embedding LLM 작업은 contextual-orchestrator 기반 Agent로 만든다. `orchestrator/free` 고정. BYTEZ_API_KEY·NVIDIA_NIM_API_KEY·NVIDIA_NIM_API_KEY_SUB·OPENROUTER_API_KEY·OPENAI_API_KEY로 auto discovery해 embedding·responses·completions·audio·video·image·omni-modal을 지원한다. 소스·adapter는 복사하지 않고 released API·client·schema로 연결한다. .github reusable workflow와 얇은 owner·consumer caller로 통합 CI를 구성한다. PR·release·consumer 변경마다 exact SHA로 build·contract·API/schema·E2E·fallback·streaming·structured output·timeout·security·SBOM·provenance를 검증한다. 결함은 owner에서 RED→fix→GREEN→release한 뒤 consumer version을 올린다. mutable sibling head·branch URL·cross-repo path·workflow 복제를 금지한다. 임시 bridge는 owner issue·만료·삭제 조건을 ADR·CI에 둔다. Provider group명은 하드코딩하지 않는다. 별칭일 뿐이며 modality·context·reasoning·tool·structured output·streaming·가격·지연·가용성·정확도 등 검증된 특성으로 선택·fallback한다. Model timeout은 공통 상한 없이 기본값을 무제한(null)로 둔다. 통신 장애는 upstream provider timeout·오류로 끝난다. 관리자 Web에서 모델별 조회·설정·해제·복원, 단위·우선순위·상속·검증·감사·API를 제공하고 설정된 모델만 제한한다. reasoning·streaming·tool call을 경과시간만으로 끊지 않으며 사용자 취소·provider 종료·관리자 timeout을 구분한다. Fugu·Conductor·TRINITY 근거로 단일·다중 Agent의 test-time compute를 단계·재귀·분해·접근·역할별 effort로 배분·ablation한다. 정확성을 우선하고 OpenCode·Strix·Noema의 모델당 2시간 이상을 수용한다. Chat은 completions·responses와 json_object·json_schema를 지원한다. Embedding은 의미 단위로 나누며 base64 이미지 인식·검색·삽입 위치·맥락을 보존한다. - -**Note (2026-09-02, supersedes the 2026-08-30 note below):** this revision states the pool pin -directly in the quoted text ("`orchestrator/free` 고정"), removing the ambiguity the original wording -had. This matches the current implemented state: `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md`'s -2026-08-30 amendment already moved Strix from `orchestrator/auto` to `orchestrator/free`, and all four -central workflows that route through `contextual-orchestrator` (`opencode-review-dispatch.yml`, -`noema-review.yml`, `strix.yml`, `pr-review-autofix.yml`) hardcode `orchestrator/free` with fail-closed -validation as of this revision (verified directly against the workflow files, 2026-09-02). No further -pool-routing change is required by this section; ADR-0003 remains the authoritative record for *why*. - -**Note (2026-08-30, on the original wording, retained for history):** the original version of this -section described `contextual-orchestrator`'s general auto-discovery capability across all five -provider secrets as a product-level design principle, which — read in isolation — an agent could -mistake for license to loosen which pool a CI consumer routes through. That ambiguity no longer applies -now that this section states the pin explicitly, but the underlying rule stands: pool/credential-scope -routing for CI consumers is governed exclusively by ADR-0003, never loosened on the strength of this -section's general wording about the orchestrator's own capabilities. - -## 9. Core foundation and development/consumption boundaries - -> Core foundation과 개발·사용 경계 @Superpowers @GitHub @Figma @Visualize @Context7 @Product Design @Consensus를 쓴다. 매 실행 README·PRD·ARCHITECTURE·release에서 책임을 확인한다. core는 완성도가 아닌 반복 수요·권위·재사용 경계로 정하고 미완성이면 owner에서 완성·release한다. transient head는 production에 쓰지 않는다. -> * .github — workflow·review/security/release owner이며 ruleset·얇은 workflow_call로만 쓴다. -> * enterprise-architecture-core·context-graph-contracts — 전사 결정·versioned context 계약 원장이며 runtime·제품 DB는 제외한다. -> * ConceptWeave·semantic-data-portal — ontology 생성·publish와 catalog·governance·소비를 분담한다. -> * contextual-orchestrator·noema — 모델 orchestration과 GitHub OIDC 단기 권한·exact-head evidence를 분담한다. -> * keyverse — identity 원장. 제품은 OIDC/OAuth·SCIM·자체 form을 쓰고 table은 복제하지 않는다. -> * EgressWeave·OriginWeave·pingora-gateway·quarantine-sandbox-runtime — outbound·browser·edge·격리 core이며 부족한 기능은 owner에서 완성한다. -> * pg-llm-batch·EmbedRelay — batch/token과 embedding identity·vector migration owner다. -> * fast-mlsirm·TEPP — IRT/MLSIRM과 다국어·시간·event·relation 측정 owner이며 kernel 재구현을 금지한다. -> * RankWeave·ThreadWeave — retrieval fusion/evaluation/TREC와 JWZ/RFC 5256 threading owner다. -> * inkspan·DiagramWeave — editor/serialization과 diagram patch/render/CLI/LSP package다. -> * mhtml-etl-gateway — MHTML 검사·schema proposal·load·lineage owner다. -> * appguardrail·wardnet — SAST/SARIF와 Rust gateway/SOC baseline owner이며 범위를 과장하지 않는다. naruon·LineageWeave·psychometrics-commons·disksage·PolicyWeave·CalendarWeave·supply-chain-control-plane은 완성도가 아니라 domain product/composition consumer라 분류한다. 공통 기능은 core owner로 추출해 통합 CI로 개발한다. +> LLM·오케스트레이션·Embedding +> LLM 작업은 contextual-orchestrator(CO) Agent로 만든다. BYTEZ_API_KEY·NVIDIA_NIM_API_KEY·NVIDIA_NIM_API_KEY_SUB·OPENROUTER_API_KEY·OPENAI_API_KEY로 auto discovery해 embedding·responses·completions·audio·video·image·omni-modal을 지원하고 released API·client·schema로 연결한다. +> 통합 CI는 .github reusable workflow와 thin caller로 구성한다. owner PR·release·consumer 변경마다 exact SHA로 build·API/schema contract·E2E·model behavior·security·SBOM·provenance를 검증한다. 결함은 owner에서 RED→fix→GREEN→release하고 consumer version을 올린다. mutable head·branch URL·cross-repo source·workflow 복제를 금지하며 bridge에는 owner issue·만료·삭제 조건을 둔다. +> GitHub Actions의 model-backed workflow는 `orchestrator/free`로 고정한다. 무료 후보 discovery·routing·fallback은 CO 내부에서만 한다. workflow는 provider·model·group명·유료 fallback을 지정하지 않고 gateway token만 쓴다. capability가 없으면 유료 우회 없이 fail closed해 free pool·contract·CI를 보완한다. +> Provider group명은 하드코딩하지 않는다. group은 별칭이며 modality·context·reasoning·tool·structured output·streaming·가격·지연·가용성·정확도 등 검증된 특성으로 선택·fallback한다. +> Model timeout은 application·Agent·Gateway 공통 상한 없이 기본 null이다. 통신 장애는 upstream provider가 끝낸다. 관리자 Web은 모델별 조회·설정·해제·복원, 단위·우선순위·상속·검증·감사·API를 제공하고 설정된 모델만 제한한다. reasoning·streaming·tool call은 시간만으로 끊지 않으며 사용자 취소·provider 종료·관리자 timeout을 구분한다. +> Fugu·Conductor·TRINITY 근거로 단일·다중 Agent의 test-time compute를 단계·재귀·분해·접근·역할별 effort로 배분·ablation한다. 정확성을 우선하고 OpenCode·Strix·Noema의 모델당 2시간 이상을 수용한다. Chat은 completions·responses와 json_object·json_schema를 지원한다. Embedding은 의미 단위로 나누고 base64 이미지의 인식·검색·삽입 위치·맥락을 보존한다. + +**Note (2026-09-02, confirmed consistent with current implementation across all three revisions):** +this section now spells out, in the quoted text itself, exactly how the `orchestrator/free` pin must be +implemented: GitHub Actions workflows select only the `orchestrator/free` virtual pool id and a gateway +token — never a concrete provider/model/group name, never a paid-fallback flag — and all free-candidate +discovery, routing, and fallback happen inside contextual-orchestrator itself; when no free capability +is available, the workflow fails closed rather than routing to a paid model. Verified directly against +`.github/workflows/strix.yml`, `noema-review.yml`, `opencode-review-dispatch.yml`, and +`pr-review-autofix.yml`: all four already match this exactly (only the virtual pool id plus a bearer +token, fail-closed on any other requested value). `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md`'s +2026-08-30 amendment already recorded the same free-only pin for Strix (superseding that ADR's original +`orchestrator/auto` decision). No further code or workflow change is required by this section; the +directive's wording and the implemented state already agree, across all three revisions. + +## 9. What "core foundation" means, and the develop/consume boundary + +> Core foundation의 의미와 개발·사용 경계 +> @Superpowers·@GitHub·@Figma·@Visualize·@Context7·@Product Design·@Consensus를 쓴다. Core foundation은 전 제품의 공통 설치물이 아니다. 여러 제품에서 반복되는 책임을 한 저장소가 canonical owner로서 독립 배포·versioned contract를 제공하는 선택형 control plane·service·library다. 보호 브랜치의 문서·API/schema·release evidence로 역할·성숙도를 확인하며 open PR은 Proposed 상태다. +> +> - 조직·계약 — .github: 공통 CI·review·security·release; enterprise-architecture-core: 전사 Context Map·architecture decision; context-graph-contracts: assertion·event·schema·fixture·conformance. domain truth는 제품에 남긴다. +> - 의미·데이터 — ConceptWeave: ontology·semantic-layer 생성·검증·release. semantic-data-portal: catalog·governance·검색·제공. EmbedRelay: embedding identity·migration. mhtml-etl-gateway: MHTML 검사·schema proposal·load lineage. +> - AI·운영 — CO: provider discovery·model capability·routing/delegation/verification/admin. noema: GitHub Actions OIDC 단기 repository capability·exact-revision evidence. pg-llm-batch: DB token count·batch 처리. +> - Identity·보안·runtime — keyverse: identity·federation·token. EgressWeave: 안전한 outbound HTTP. OriginWeave: governed browser. pingora-gateway: Rust edge. quarantine-sandbox-runtime: 격리. appguardrail: scan·SARIF·remediation. wardnet: gateway·WAF·IDS·SOC. +> - 재사용 기능 — fast-mlsirm: IRT·MLSIRM. TEPP: 다국어·시간·event·relation 측정. RankWeave: retrieval fusion·evaluation·통계 비교·tuning·TREC. ThreadWeave: JWZ/RFC 5256 threading. inkspan: editor·serialization·문서 변환. DiagramWeave: diagram patch·render·CLI·LSP. +> +> owner가 미성숙하거나 API가 없어도 consumer가 복제·우회하지 않는다. owner에서 RED test→기능·문서·release를 개발해 CI GREEN과 immutable version을 낸 뒤 채택한다. 그 전에는 port·ACL·feature flag·test double로 경계를 지키고 owner의 source·DB·임시 branch를 직접 읽지 않는다. + +**Note (2026-09-02, third revision):** this revision adds a definition of what "core foundation" means +that the second revision did not state explicitly: it is *not* a common install for every product, but +a **selective** control plane/service/library that one repo canonically owns because a responsibility +repeats across multiple products, providing independent deployment and versioned contracts — role and +maturity are verified from protected-branch docs/API-schema/release evidence, and an open PR proposing +core status is `Proposed`, not yet authoritative. This revision also groups the repo list under five +named categories (조직·계약 / 의미·데이터 / AI·운영 / Identity·보안·runtime / 재사용 기능) instead of the +second revision's flat bullet list, and adds explicit interim-boundary mechanisms for the "build the +core, don't bypass it" policy first stated in §2: while an owner repo is still building out a needed +capability, a consumer holds the boundary with a port, an ACL, a feature flag, or a test double — and +must not read the owner's source, database, or a temporary branch directly. All repository names below +were re-checked against this revision's own list and match the second revision's already-verified set +exactly (no new or renamed repos introduced). **Verification (2026-09-02):** every repository named above was confirmed to exist under `ContextualWisdomLab` with exact-matching case via a direct GitHub repository listing on the date of this revision: `.github`, `enterprise-architecture-core`, `context-graph-contracts`, `ConceptWeave`, `semantic-data-portal`, `contextual-orchestrator`, `noema`, `keyverse`, `EgressWeave`, `OriginWeave`, `pingora-gateway`, `quarantine-sandbox-runtime`, `pg-llm-batch`, `EmbedRelay`, `fast-mlsirm`, `TEPP`, -`RankWeave`, `ThreadWeave`, `inkspan`, `DiagramWeave`, `mhtml-etl-gateway`, `appguardrail`, `wardnet`, -`naruon`, `LineageWeave`, `psychometrics-commons`, `disksage`, `PolicyWeave`, `CalendarWeave`, -`supply-chain-control-plane`. None needed a spelling or case correction. +`RankWeave`, `ThreadWeave`, `inkspan`, `DiagramWeave`, `mhtml-etl-gateway`, `appguardrail`, `wardnet`. +The second revision additionally named `naruon`, `LineageWeave`, `psychometrics-commons`, `disksage`, +`PolicyWeave`, `CalendarWeave`, `supply-chain-control-plane` as domain-product/composition consumers +(not core); the third revision's §9 text above does not restate that consumer list explicitly, but its +opening definition ("Core foundation은 전 제품의 공통 설치물이 아니다") implies the same classification. +All 29 names across both revisions were verified to exist with matching case; none needed a spelling or +case correction. **Open reconciliation item (2026-09-02, not yet resolved — do not silently pick one):** `docs/CWL-MASTER-CONTEXT.md` §3 ("Ecosystem components") already documents roles for a subset of these repos (`contextual-orchestrator`, `pg-llm-batch`, `fast-mlsirm`, `semantic-data-portal`, `noema`, `appguardrail`, `inkspan`, `keyverse`, `wardnet`, among others) with narrower or differently-framed -descriptions than this section, and does not yet mention most of the repos newly named here -(`ConceptWeave`, `enterprise-architecture-core`, `context-graph-contracts`, `EgressWeave`, -`OriginWeave`, `pingora-gateway`, `quarantine-sandbox-runtime`, `EmbedRelay`, `DiagramWeave`, -`mhtml-etl-gateway`, `PolicyWeave`, `CalendarWeave`, `supply-chain-control-plane`, -`psychometrics-commons`, `LineageWeave`) at all. One specific tension worth flagging rather than -silently resolving: §3 currently describes `noema` as owning "the lightweight quarantine sandbox", -while this section groups `quarantine-sandbox-runtime` as its own dedicated repo alongside -`EgressWeave`/`OriginWeave`/`pingora-gateway` under "outbound·browser·edge·격리 core" — whether the -sandbox responsibility moved to a dedicated repo, or the two repos share it, has not been verified -against either repo's actual current content as of this revision. A future pass should read both -repos' READMEs/ARCHITECTURE docs and either update §3 to match this section, update this section if §3 -is the one that's current, or record an ADR if the split is a genuinely new decision — not guess. +descriptions than this section, and does not yet mention most of the repos named here (`ConceptWeave`, +`enterprise-architecture-core`, `context-graph-contracts`, `EgressWeave`, `OriginWeave`, +`pingora-gateway`, `quarantine-sandbox-runtime`, `EmbedRelay`, `DiagramWeave`, `mhtml-etl-gateway`) at +all. One specific tension worth flagging rather than silently resolving: §3 currently describes `noema` +as owning "the lightweight quarantine sandbox," while this section groups `quarantine-sandbox-runtime` +as its own dedicated repo alongside `EgressWeave`/`OriginWeave`/`pingora-gateway` under +"Identity·보안·runtime" — whether the sandbox responsibility moved to a dedicated repo, or the two repos +share it, has not been verified against either repo's actual current content as of this revision. A +future pass should read both repos' READMEs/ARCHITECTURE docs and either update §3 to match this +section, update this section if §3 is the one that's current, or record an ADR if the split is a +genuinely new decision — not guess. ## How to point a `/goal` session at this directive @@ -155,7 +222,7 @@ Because `/goal` truncates at 4000 characters, do not paste the sections above in short pointer, e.g. (Korean, ~260 chars, well under the cap): ```text -/goal ContextualWisdomLab/.github의 docs/product-goal-directive.md 전문을 지침으로 삼아 실행하라. 열린 PR마다 리뷰 확인→수정→Checks 재검증→병합→다음 개발을 중간 보고 없이 반복하고, PR·Issue 소진 후에도 Gap 기반 개발을 계속한다. 이 문서의 9개 절 전체(실행 루프, 동시작업/근본수정, 연구추적성, UX/UI/i18n, 아키텍처/온톨로지/DB, 언어/측정, 검증/부하, LLM/오케스트레이션, Core foundation과 개발/사용 경계)를 매 사이클 적용 대상으로 취급하고, 이 문서와 docs/CWL-MASTER-CONTEXT.md가 상충하면 상충을 해소하고 두 문서를 함께 갱신하라. 매시간 재예약하라. +/goal ContextualWisdomLab/.github의 docs/product-goal-directive.md 전문을 지침으로 삼아 실행하라. 열린 PR마다 리뷰 확인→수정→Checks 재검증→병합→다음 개발을 중간 보고 없이 반복하고, PR·Issue 소진 후에도 Gap 기반 개발을 계속한다. 이 문서의 9개 절 전체(실행 루프, 동시작업/근본수정, 연구추적성, UX/UI/i18n, 아키텍처/온톨로지/DB, 언어/측정, 검증/부하, LLM/오케스트레이션, core foundation의 의미와 개발/사용 경계)를 매 사이클 적용 대상으로 취급하고, 이 문서와 docs/CWL-MASTER-CONTEXT.md가 상충하면 상충을 해소하고 두 문서를 함께 갱신하라. 매시간 재예약하라. ``` When this directive itself changes (the user revises a section, or an agent finds it conflicts with @@ -168,16 +235,22 @@ When this directive itself changes (the user revises a section, or an agent find against `docs/CWL-MASTER-CONTEXT.md` (the wardnet naming-history conflict in §5, the `orchestrator/auto`-vs-`orchestrator/free` pool-routing ambiguity in §8); see `docs/doctoring/product-goal-directive.md` for the full PR #1429 review-finding trail. -- **2026-09-02** — the owner reissued the directive with all nine sections substantially expanded: - §2 added an explicit "build the immature core, don't bypass it" policy (RED-test-driven development - at the owner repo, ADR-gated exceptions only); §4 added detailed i18n requirements (8 supported - languages, DB-backed versioned translation resources, no full-catalog client bundles) and clarified - Keycloak's role (auth backend only, product-owned login/signup/recovery forms); §5 broadened the - naming rule from DB objects to all code identifiers and added an ontology-ownership split across four - named repos; §8 folded the `orchestrator/free` pin directly into the quoted text, resolving the prior - ambiguity; §9 replaced the general reference-library list with a full named core-repo table and an - explicit core-vs-domain-product classification. Recorded verbatim, superseding the 2026-08-30 text of - every section; both the 2026-08-30 and 2026-09-02 reconciliation notes are kept inline above (each - marked with its date) so neither the original context nor the correction is lost. See - `docs/doctoring/product-goal-directive.md` for this revision's own reconciliation record, including - the open §3-vs-§9 repo-role item noted under §9 above. +- **2026-09-02 (second revision)** — the owner reissued the directive with all nine sections + substantially expanded: §2 added an explicit "build the immature core, don't bypass it" policy; §4 + added detailed i18n requirements and clarified Keycloak's role; §5 broadened the naming rule to all + code identifiers and added an ontology-ownership split across four named repos; §8 folded the + `orchestrator/free` pin directly into the quoted text; §9 replaced the general reference-library list + with a full named core-repo table and an explicit core-vs-domain-product classification. Recorded in + ContextualWisdomLab/.github#1692. +- **2026-09-02 (third revision, this file's current text)** — the owner issued one further refinement + the same day: §1 clarified that "0 open PRs" must come from merges or full successor-absorption, never + a bare close; §2 added the substantial repair-vs-close PR-lifecycle policy (single-writer/DDD + violations, wrong base/conflicts, ADR-number collisions, premature `Accepted`, unprotected + dependencies, and missing test/fixture/contract are repair findings, not grounds for closing); §4 + dropped the named-framework shortlist in favor of framework-agnostic selection criteria; §5 moved the + detailed per-repo ontology-owner breakdown into §9; §8 spelled out exactly how GitHub Actions + workflows must implement the `orchestrator/free` pin (pool id + gateway token only, fail-closed, no + provider/model/group name, no paid fallback); §9 added an explicit definition of what "core + foundation" means, reorganized the repo table into five named categories, and added concrete + interim-boundary mechanisms (port/ACL/feature-flag/test-double) for the build-the-core policy. See + `docs/doctoring/product-goal-directive.md` for this revision's reconciliation record. From 9046e4d46229b0a3613cf669f1c0b1c856de596f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 13:20:58 +0000 Subject: [PATCH 3/7] fix(directive): address Devin Review findings on #1692 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three confirmed findings from Devin Review's pass on PR #1692, fixed in place: - docs/product-goal-directive.md §5's rename-reconciliation note told an agent to "pause and confirm" before a contract-breaking rename, which directly contradicts docs/CWL-MASTER-CONTEXT.md §7's binding "Do NOT ask the user to decide" full-autonomy convention. Replaced with an autonomous, contract-safe process (preserve the published boundary, prepare a versioned migration plan, record an ADR) instead of asking. - The repo-count arithmetic in docs/product-goal-directive.md §9 and its doctoring record was wrong: §9 actually lists 23 core repos (not 21), and the four ontology repos named in §5 are a named subset of those 23, not an additional group — so the correct total is 23 + 7 domain-consumer repos = 30 unique names, not 29. Fixed both files' counts and the double-counting framing. - The §9 "sandbox ownership" reconciliation item was left flagged as open rather than resolved. Read quarantine-sandbox-runtime's and noema's actual current READMEs (plus noema's own sandbox-planning doc, which states the review agent runs in "a separate quarantined execution plane" and must not run untrusted code in the Noema Worker process): quarantine-sandbox-runtime is the current AI-SOC sandbox owner, not noema. Updated docs/CWL-MASTER-CONTEXT.md at all four locations that said "noema quarantine sandbox" (§3, §6 header, the P1 roadmap bullet, and the ecosystem UML diagram's node/edge) to name quarantine-sandbox-runtime instead, and marked the directive's reconciliation note resolved. docs/doctoring/product-goal-directive.md records the full reasoning for all six Devin findings (three fixed, one requiring cross-repo evidence, two informational/no-action) per this file's existing pattern from the original PR #1429 Devin Review pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN --- docs/CWL-MASTER-CONTEXT.md | 22 +++++-- docs/doctoring/product-goal-directive.md | 77 ++++++++++++++++++++++-- docs/product-goal-directive.md | 53 +++++++++------- 3 files changed, 119 insertions(+), 33 deletions(-) diff --git a/docs/CWL-MASTER-CONTEXT.md b/docs/CWL-MASTER-CONTEXT.md index 2ce09ee89e..558b7049a9 100644 --- a/docs/CWL-MASTER-CONTEXT.md +++ b/docs/CWL-MASTER-CONTEXT.md @@ -33,7 +33,7 @@ Product renames (repo slug → product name; domains purchased): `cwl-idp`→**k - **codec-carver** — STT / omni-modal speech+video codec (audio/video conversion for LLM input); speaker diarization + consented voiceprint; feeds auto meeting minutes. - **fast-mlsirm** — LLM-as-a-Judge output **calibration** + measurement/evaluation-item quality; incorporate `aFIPC` Fixed-Item Parameter Calibration + `kaefa`-style item-fit optimal-model search (R IRT/psychometrics). GPU = GPGPU in the Rust core (wgpu, single numpy|rust backend axis). - **semantic-data-portal (SDP)** — the higher **ontology / catalog / governance plane** ABOVE the doc KG (Apache AGE + pgvector). naruon owns the doc KG (content_graph + project_graph in Postgres); SDP is not that store. -- **noema** — agent runtime (Pydantic-AI / Codex-Python): a GitHub Review Agent in CI + a do-anything agent inside naruon + the **lightweight quarantine sandbox**. +- **noema** — agent runtime (Pydantic-AI / Codex-Python): a GitHub Review Agent in CI + a do-anything agent inside naruon. Untrusted artifact/code analysis runs in a separately quarantined execution plane, owned by **quarantine-sandbox-runtime** (see §6), not inside the Noema Worker process (see §6's note, 2026-09-02). - **newsdom-api** — PDF → DOM recognition sidecar (generalized beyond JP newspapers). naruon parses non-PDF formats (html/md/plaintext) into its content_graph. - **scopeweave** — issue/WBS **management** + ITSM Service Request (two-layer: requester ticket ↔ team issues). Consumes issues naruon extracts from email/conversation/ITSR. (Dev-CODE issues stay in GitHub/GitLab — integrate, don't rebuild GitHub.) - **appguardrail** — app security guardrails; collects org security/CI failures + Strix findings as issues. @@ -97,7 +97,18 @@ Literature-grounded (see papers below). A dyad can hold MULTIPLE relationship ty **SCOPE — the SOCIAL GRAPH belongs IN the KG (core); only ENFORCEMENT is out (per §1b).** Do NOT confuse "naruon doesn't run org workflows" with "drop the social network" — the relationship / social-network graph is CORE and lives fully in the KG (it already does: naruon `project_graph_objects` has a `participant` type + the DAG Sender Ontology + domain-model-realignment). The social graph is the FOUNDATION for the origin jobs AND the real pains: it powers FIND + PRIORITIZE (DAG sender ontology — "what this sender means to me"), schedule TRACKING (who a changing meeting is with + priority), and — critically — the project pains the user named: **too many projects, schedule management that doesn't work, WBS that can't be estimated, Job/Work/Task/Duty that is a mystery.** The person↔person + person↔event + dependency graph is exactly what makes schedule management, **WBS / inter-event dependency ESTIMATION**, and work decomposition (Job/Work/Task/Duty) possible (with scopeweave). The ONLY out-of-scope part is naruon EXECUTING org actions (auto-recuse, route approvals, run 전자결재/HR): naruon **models + reasons + surfaces**, the external systems ACT. So: full social-graph modeling + inference + estimation-support + surfacing = IN; workflow enforcement = OUT. **Attachable papers (CC BY 4.0, redistributable):** Higgins, Crepalde & Fernandes (2021) PLOS ONE 16(9):e0257527 (segmented multiplexity); Frontiers in Psychology (2021) 12:690074 (ambivalent leader-follower). Green-OA (link, don't redistribute): Levin et al. 2011 Organization Science (dormant ties); Pierce/Byrne/Aguinis 1996 JOB (workplace-romance power differential). Cite-only (copyright): Verbrugge 1979, Merton 1957, Kahn 1964, Kram 1983, Jaskiewicz 2013. -## 6. AI SOC = wardnet + noema quarantine sandbox (see wardnet#38) +## 6. AI SOC = wardnet + quarantine-sandbox-runtime (see wardnet#38) + +**Note (2026-09-02):** this section's spec previously said "noema quarantine sandbox." Verified against +both repos' current READMEs: `quarantine-sandbox-runtime` describes itself, near-verbatim, as this +section's own spec ("Source-agnostic, credential-free artifact analysis runtime for the +ContextualWisdomLab security ecosystem"), while `noema`'s current README describes an unrelated +product — an "evidence-producing credential and maintenance control plane" for GitHub OIDC/App-token +exchange and review evidence — with no artifact-analysis or sandbox responsibility, and +`noema/docs/noema-agent-sandbox-plan.md` explicitly states the review agent "runs in a separate +quarantined execution plane" that "must not run untrusted repository code in the Noema Worker process." +The sandbox responsibility this section describes belongs to `quarantine-sandbox-runtime`, not `noema`. + A **source-agnostic artifact-analysis service**: `submit(artifact, context) → {verdict, confidence, evidence, IOCs}`. Consumers: naruon email/file attachments (quarantine BEFORE store), platform uploads, connector inputs, API, GitHub issue/PR comments (one trigger). WITHOUT VirusTotal (self-contained): static (YARA(BSD) + capa(Apache) capability→ATT&CK + LIEF/pefile + unzip/macro extract + entropy + context heuristics) + dynamic detonation in a gVisor/Firecracker (Apache) microVM with eBPF behavioral monitoring (Falco/Tetragon, Apache) + network sinkhole + **LLM reasoning (via contextual-orchestrator) over the evidence** + KG/IOC correlation (self-hosted growing reputation). Auto-response per consumer (GitHub → delete comment + block user; email → quarantine + flag; upload → reject + notify). Validated by a real incident 2026-07-08 (user mapasevo21 posted a `sarif_bypass_patch.zip` malware lure on .github#365 + naruon#977 — deleted + blocked manually; this is what the SOC would automate). ## 7. Engineering conventions (BINDING, all agents) @@ -114,7 +125,7 @@ A **source-agnostic artifact-analysis service**: `submit(artifact, context) → ## 8. Roadmap (full detail: naruon#974 §9; live status: Project #1) - **P0 MVP** — make the dense KG real (behind a stable extractor seam; do NOT productionize the deterministic stopgap; reconcile multi-account model; extend hybrid search to content_segments + project_graph_objects; wire DecisionPointCard). -- **P1 Platform/Plugin SDK** — registry, versioned API, hook bus, manifest/license/signature gate, noema quarantine sandbox, /plugins UI. +- **P1 Platform/Plugin SDK** — registry, versioned API, hook bus, manifest/license/signature gate, quarantine-sandbox-runtime (see §6), /plugins UI. - **P2 Dense-KG inference** — LLM-based language-agnostic extraction (orchestrator-routed) + batch embeddings; typed entities (graph_persons/events/commitments, norm_groups + memberships); prior×likelihood posterior; no-ask auto-resolve + correct-by-exception. - **P3 Scheduling & conflict avoidance** — status-weighted conflict engine; iTIP/iMIP RSVP (organizer vs attendee); free-busy find-time; room booking; anticipatory 전자결재→travel; connector CardDAV + POP3-over-WS. - **P4 Privacy bridge** — context isolation + content-based classification; consent minimal-disclosure bridge. @@ -177,7 +188,8 @@ flowchart TB SCOPE["scopeweave — issues / WBS / ITSM"] CODEC["codec-carver — STT / audio→minutes (+voiceprint)"] BAND["bandscope — musicians' rehearsal vertical"] - NOEMA["noema — agent runtime + quarantine sandbox"] + NOEMA["noema — agent runtime + review evidence"] + QUAR["quarantine-sandbox-runtime — AI SOC sandbox (§6)"] end subgraph INFRA["Infra / governance"] @@ -213,7 +225,7 @@ flowchart TB NAR -->|"extracted issues → manage"| SCOPE CODEC -->|"diarize + minutes"| NAR NAR --> NOEMA - WARD -->|"quarantine detonation"| NOEMA + WARD -->|"quarantine detonation"| QUAR BAND -->|"musicians also use email"| NAR BAND -. "rehearsal app" .-> P2 diff --git a/docs/doctoring/product-goal-directive.md b/docs/doctoring/product-goal-directive.md index b3bee6c279..3cdcb71367 100644 --- a/docs/doctoring/product-goal-directive.md +++ b/docs/doctoring/product-goal-directive.md @@ -151,7 +151,7 @@ fixed: needing an out-of-band note to resolve it. - §9 (renamed from "Reference libraries and tool invocations" to "Core foundation and development/consumption boundaries") replaced the general - reference-library bullet list with an explicit table of 21 named + reference-library bullet list with an explicit table of 23 named core-owner repos grouped by responsibility (workflow/review/security/ release; enterprise architecture/context contracts; ontology generation vs. catalog/governance; model orchestration/OIDC/evidence; identity; @@ -166,11 +166,14 @@ fixed: ### Verification performed this revision -1. **Repo-existence check.** Every one of the 29 repositories named across - §5 and §9 (the four ontology repos, all 21 §9 core repos, and the seven - §9 domain-consumer repos) was checked against a live listing of every - `ContextualWisdomLab` repository the operating account can reach. All 29 - matched exactly, including case (e.g. `ConceptWeave`, `EgressWeave`, +1. **Repo-existence check.** Every one of the 30 repositories named across + §5 and §9 (the §5 ontology-ownership discussion names four of these — + `ConceptWeave`, `semantic-data-portal`, `context-graph-contracts`, + `enterprise-architecture-core` — as a named subset of, not additional to, + the 23 §9 core repos; plus the seven §9 domain-consumer repos) was + checked against a live listing of every `ContextualWisdomLab` repository + the operating account can reach. All 30 matched exactly, including case + (e.g. `ConceptWeave`, `EgressWeave`, `OriginWeave`, `DiagramWeave`, `PolicyWeave`, `CalendarWeave` — all PascalCase, confirmed correct as given, not "corrected" to another case). None needed a spelling or case fix. This check is recorded @@ -355,3 +358,65 @@ fixed: carrying both the second and third revision's commits; not superseded by a new PR, per this directive's own "edit this file in place... do not fork a second copy elsewhere" policy applied to the PR-branch level too. + +## 2026-09-02 Devin Review findings on PR #1692 — three confirmed, fixed + +Devin Review's automated pass on #1692 raised six findings; three were confirmed real and fixed in +place (not by editing the verbatim quoted directive text), one required cross-repo verification beyond +this directive's own file, and two were informational/no-action. Following the same pattern as the +2026-08-30 Devin Review findings on the original PR #1429 (see above): confirm before fixing, fix in the +notes rather than the quoted blockquotes, and record the reasoning here. + +1. **Confirmed — §5's "pause and confirm" rename language contradicted + `docs/CWL-MASTER-CONTEXT.md` §7's binding "Do NOT ask the user to decide — make the call and proceed" + (full autonomy) convention.** The reconciliation note added by the third revision (on breaking a + published contract) said such a rename was "exactly the kind of consequential, hard-to-reverse action + this org's own engineering conventions ask an agent to pause and confirm before taking" — that framing + is simply wrong; §7 forbids asking the user to decide, full stop. Fixed by replacing "pause and + confirm" with an autonomous, contract-safe process: preserve the existing published boundary (alias/ + deprecation shim/versioned API), prepare a versioned migration and compatibility plan, and record the + rename as an ADR — proceeding without waiting on a human, with explicit approval required only where + some *other* already-existing policy demands it for a specific irreversible action (this repo's own + "never force-push over someone else's branch," for example), not as a new exception this note invents. +2. **Confirmed — repository-count arithmetic did not reconcile.** The doctoring text for the second + revision (above) said §9 listed "21 named core-owner repos" and that "29" total names were verified + across "the four ontology repos, all 21 §9 core repos, and the seven §9 domain-consumer repos" — but + §9's actual list (verified by recounting it directly) has 23 entries, not 21, and the four ontology + repos named in §5 (`ConceptWeave`, `semantic-data-portal`, `context-graph-contracts`, + `enterprise-architecture-core`) are a **named subset of**, not additional to, those 23 — so treating + them as a fifth, separate group of "4" was double-counting on top of an already-wrong base count. The + correct total is 23 §9 core repos + 7 §9 domain-consumer repos = 30 unique names, not 29. Fixed both + this doctoring file's second-revision section (above) and `docs/product-goal-directive.md`'s own §9 + "Verification" note to say 23 and 30, and to state the four-ontology-repos-are-a-subset relationship + explicitly instead of implying a fifth additive group. +3. **Confirmed — sandbox ownership tension was left "open, not yet resolved" when it was directly + resolvable.** The prior revision's §9 note flagged, but deliberately did not resolve, the tension + between `docs/CWL-MASTER-CONTEXT.md` (`noema` owns "the lightweight quarantine sandbox") and this + directive's §9 (`quarantine-sandbox-runtime` as its own dedicated repo) — correctly declining to guess, + but Devin's finding was right that this PR could resolve it with the reading it had asked for the last + time, rather than deferring again. Resolved this revision by cloning both repos read-only and reading + `noema/README.md`, `quarantine-sandbox-runtime/README.md`, and `noema/docs/noema-agent-sandbox-plan.md` + directly: `quarantine-sandbox-runtime`'s README ("Source-agnostic, credential-free artifact analysis + runtime for the ContextualWisdomLab security ecosystem") is a near-verbatim match for + `CWL-MASTER-CONTEXT.md` §6's own AI-SOC sandbox spec; `noema`'s current README is an unrelated product + (GitHub OIDC/App-token credential exchange and review evidence, explicitly disclaiming model/provider + ownership) with no artifact-analysis responsibility; and `noema`'s own sandbox-planning doc states the + review agent "runs in a separate quarantined execution plane" that "must not run untrusted repository + code in the Noema Worker process" — i.e. noema's own architecture decision already separates sandbox + execution out of its own process. Fixed by updating `docs/CWL-MASTER-CONTEXT.md` at all four locations + that said "noema quarantine sandbox" (§3's noema bullet, §6's header, the P1 roadmap bullet, and the + ecosystem UML diagram's node label plus the `WARD -->|"quarantine detonation"|` edge target) to name + `quarantine-sandbox-runtime` instead, and updating this directive's §9 note from "open, not yet + resolved" to "resolved," while explicitly leaving open (as a coverage gap, not a contradiction) that + §3 still doesn't mention most of §9's other newer repos at all. +4. **Informational, no action — empty-PR cleanup remains permitted.** Devin correctly read §2's closing + conditions as still permitting closure of a non-draft, zero-changed-file PR; this is consistent with + "no valid delta remains" and needed no change. +5. **Informational, no action — §9's quoted blockquote doesn't restate the seven-repo consumer + classification verbatim.** True, but by design: the blockquote is the owner's verbatim wording (never + paraphrased, per this file's own established pattern above) and the classification is preserved in the + "Verification" note immediately below it, which is exactly what that note is for. No fix applied; the + quoted text is not editable for this reason without violating the file's own verbatim-preservation + rule. +6. **Informational, no action — pool pin matches deployed workflows.** Devin's own check confirmed §8's + text already matches the four live workflow files. No action needed. diff --git a/docs/product-goal-directive.md b/docs/product-goal-directive.md index 3d816f337a..493c5fb3a8 100644 --- a/docs/product-goal-directive.md +++ b/docs/product-goal-directive.md @@ -110,10 +110,17 @@ specifically and still grandfathers existing Camel/Pascal DB objects; nothing in that clause. Read this section's broader identifier rule as prospective and opportunistic: apply it to new code, and fix existing violations incrementally when touching that code for another reason — not as a mandate to sweep every existing repo renaming public APIs, exported classes, or shipped DB objects in -one pass. A rename that breaks a published contract (a public API signature, a released client, an -external integration) is exactly the kind of consequential, hard-to-reverse action this org's own -engineering conventions ask an agent to pause and confirm before taking, not something "위반명은 -치환한다" authorizes unilaterally at scale. +one pass. A rename that breaks a published contract (a public API signature, a released client, an external +integration) is not something "위반명은 치환한다" authorizes as an unversioned, unrecorded rewrite at +scale — but `docs/CWL-MASTER-CONTEXT.md` §7's binding convention ("Do NOT ask the user to decide — make +the call and proceed," full autonomy) still governs: an agent does not pause and wait for a human to +confirm this. The autonomous, contract-safe process is instead: preserve the existing published +boundary (keep the old name/signature working — an alias, a deprecation shim, a versioned API), prepare +a versioned migration and compatibility plan for consumers, and record the rename as an ADR so the +decision and its rationale are durable. Explicit human approval is required only where some other +already-existing policy demands it for a specific irreversible external action (e.g. this repo's own +"never force-push over someone else's branch," or an operation with no compensating rollback) — this +rename note does not itself create a new ask-first exception to §7. **Note (2026-08-30, on the original wording, retained for history):** the original (2026-08-30) version of this section named "wardnet" as an example of an "old name" (옛 이름) to rename *away @@ -197,24 +204,26 @@ The second revision additionally named `naruon`, `LineageWeave`, `psychometrics- `PolicyWeave`, `CalendarWeave`, `supply-chain-control-plane` as domain-product/composition consumers (not core); the third revision's §9 text above does not restate that consumer list explicitly, but its opening definition ("Core foundation은 전 제품의 공통 설치물이 아니다") implies the same classification. -All 29 names across both revisions were verified to exist with matching case; none needed a spelling or -case correction. - -**Open reconciliation item (2026-09-02, not yet resolved — do not silently pick one):** -`docs/CWL-MASTER-CONTEXT.md` §3 ("Ecosystem components") already documents roles for a subset of these -repos (`contextual-orchestrator`, `pg-llm-batch`, `fast-mlsirm`, `semantic-data-portal`, `noema`, -`appguardrail`, `inkspan`, `keyverse`, `wardnet`, among others) with narrower or differently-framed -descriptions than this section, and does not yet mention most of the repos named here (`ConceptWeave`, -`enterprise-architecture-core`, `context-graph-contracts`, `EgressWeave`, `OriginWeave`, -`pingora-gateway`, `quarantine-sandbox-runtime`, `EmbedRelay`, `DiagramWeave`, `mhtml-etl-gateway`) at -all. One specific tension worth flagging rather than silently resolving: §3 currently describes `noema` -as owning "the lightweight quarantine sandbox," while this section groups `quarantine-sandbox-runtime` -as its own dedicated repo alongside `EgressWeave`/`OriginWeave`/`pingora-gateway` under -"Identity·보안·runtime" — whether the sandbox responsibility moved to a dedicated repo, or the two repos -share it, has not been verified against either repo's actual current content as of this revision. A -future pass should read both repos' READMEs/ARCHITECTURE docs and either update §3 to match this -section, update this section if §3 is the one that's current, or record an ADR if the split is a -genuinely new decision — not guess. +All 30 names across both revisions (23 §9 core repos, of which 4 are also named in §5's ontology +discussion, plus the 7 §9 domain-consumer repos) were verified to exist with matching case; none needed +a spelling or case correction. + +**Reconciliation item — sandbox ownership (resolved 2026-09-02):** an earlier draft of this note flagged +an unresolved tension between `docs/CWL-MASTER-CONTEXT.md` §3/§6 (which said `noema` owns "the +lightweight quarantine sandbox") and this section (which lists `quarantine-sandbox-runtime` as its own +dedicated repo). Resolved by reading both repos' current READMEs/plans directly, not guessing: +`quarantine-sandbox-runtime`'s README describes itself, almost verbatim, as `CWL-MASTER-CONTEXT.md` +§6's own AI-SOC sandbox spec ("source-agnostic, credential-free artifact analysis runtime"); `noema`'s +current README describes an unrelated product (an evidence-producing credential/maintenance control +plane for GitHub OIDC/App-token exchange and review evidence, with no artifact-analysis responsibility), +and `noema/docs/noema-agent-sandbox-plan.md` explicitly states the review agent "runs in a separate +quarantined execution plane" that "must not run untrusted repository code in the Noema Worker process." +`docs/CWL-MASTER-CONTEXT.md` §3/§6/§8/the ecosystem UML diagram were updated in this same PR to name +`quarantine-sandbox-runtime`, not `noema`, as the sandbox owner. The remaining gap this note originally +flagged — §3 not yet mentioning `ConceptWeave`, `enterprise-architecture-core`, +`context-graph-contracts`, `EgressWeave`, `OriginWeave`, `pingora-gateway`, `EmbedRelay`, `DiagramWeave`, +or `mhtml-etl-gateway` at all — is still open and left for a future pass; it is a coverage gap, not a +contradiction, so it does not block this revision. ## How to point a `/goal` session at this directive From 53e85f0e0fe7db8714f23a119892b57a7295514f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 13:23:53 +0000 Subject: [PATCH 4/7] fix(directive): fix residual stale repo count in third-revision doctoring section Devin Review caught a residual inconsistency the previous fix missed: the doctoring file's third-revision section ("What changed in the directive text" and its own "Verification performed this revision") repeated the same stale "21 core repos" figure twice, independently of the second-revision section already corrected to 23. Fixes both remaining occurrences. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN --- docs/doctoring/product-goal-directive.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/product-goal-directive.md b/docs/doctoring/product-goal-directive.md index 3cdcb71367..443f666e5e 100644 --- a/docs/doctoring/product-goal-directive.md +++ b/docs/doctoring/product-goal-directive.md @@ -315,7 +315,7 @@ fixed: library one repo canonically owns because a responsibility repeats across products, with role/maturity verified from protected-branch evidence, and an open PR proposing core status is `Proposed`, not authoritative yet); - reorganized the same 21-repo list into five named categories (조직·계약 / + reorganized the same 23-repo list into five named categories (조직·계약 / 의미·데이터 / AI·운영 / Identity·보안·runtime / 재사용 기능) instead of the second revision's flat bullet list; and added concrete interim-boundary mechanisms for the §2 build-the-core policy — while an owner is still @@ -326,7 +326,7 @@ fixed: ### Verification performed this revision 1. **Repo list unchanged, re-confirmed.** The third revision's §9 names the - same 21 core repos as the second revision (reorganized into categories, + same 23 core repos as the second revision (reorganized into categories, not expanded or reduced), plus the same second-revision consumer set is implied by §9's new opening definition even though the third revision's quoted text doesn't restate the seven-repo consumer list verbatim. No new @@ -388,7 +388,11 @@ notes rather than the quoted blockquotes, and record the reasoning here. correct total is 23 §9 core repos + 7 §9 domain-consumer repos = 30 unique names, not 29. Fixed both this doctoring file's second-revision section (above) and `docs/product-goal-directive.md`'s own §9 "Verification" note to say 23 and 30, and to state the four-ontology-repos-are-a-subset relationship - explicitly instead of implying a fifth additive group. + explicitly instead of implying a fifth additive group. **Follow-up (same day):** the first pass of this + fix missed that the third-revision section further below ("What changed in the directive text" and its + own "Verification performed this revision") repeated the same stale "21" figure twice, independently of + the second-revision section already fixed — Devin Review caught this residual inconsistency on the next + push. Fixed both remaining occurrences to say 23, matching the corrected count everywhere in this file. 3. **Confirmed — sandbox ownership tension was left "open, not yet resolved" when it was directly resolvable.** The prior revision's §9 note flagged, but deliberately did not resolve, the tension between `docs/CWL-MASTER-CONTEXT.md` (`noema` owns "the lightweight quarantine sandbox") and this From 61104eabac120e3751b1daec399538ff0d689fb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:34:20 +0900 Subject: [PATCH 5/7] fix(directive): preserve current protected-main tree during reconciliation --- .github/workflows/dependency-review.yml | 163 ++++ .github/workflows/noema-review.yml | 23 + .../noema-token-lifetime-quality-ci.yml | 12 +- .github/workflows/opencode-review.yml | 2 +- .github/workflows/pr-review-autofix.yml | 16 + .github/workflows/strix.yml | 13 + CHANGELOG.md | 18 + ...22-scheduler-active-workflow-runs-cache.md | 174 ++++ ...-review-reusable-workflow-consolidation.md | 167 ++++ ...-noema-review-model-job-timeout-removal.md | 91 +++ .../ci-workflow-duplication-audit-20260902.md | 130 +++ ...-review-reusable-workflow-consolidation.md | 264 ++++++ ...ema-token-lifetime-stale-run-retirement.md | 39 + ...scheduler-stale-headrefoid-cancellation.md | 46 ++ scripts/ci/pr_review_merge_scheduler.py | 269 ++++++- ...dency_review_reusable_workflow_contract.py | 142 ++++ ...st_noema_orchestrator_workflow_contract.py | 57 ++ ...noema_token_lifetime_stale_run_contract.py | 22 + tests/test_opencode_poll_self_retirement.py | 9 +- .../test_pr1669_cancel_stale_opencode_runs.py | 154 ++++ ...review_autofix_writer_security_contract.py | 31 + tests/test_pr_review_merge_scheduler.py | 753 +++++++++++++++++- tests/test_strix_openai_fallback_api_base.py | 21 + 23 files changed, 2573 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/dependency-review.yml create mode 100644 docs/adr/0022-scheduler-active-workflow-runs-cache.md create mode 100644 docs/adr/0024-dependency-review-reusable-workflow-consolidation.md create mode 100644 docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md create mode 100644 docs/doctoring/ci-workflow-duplication-audit-20260902.md create mode 100644 docs/doctoring/dependency-review-reusable-workflow-consolidation.md create mode 100644 docs/doctoring/noema-token-lifetime-stale-run-retirement.md create mode 100644 docs/doctoring/scheduler-stale-headrefoid-cancellation.md create mode 100644 tests/test_dependency_review_reusable_workflow_contract.py create mode 100644 tests/test_noema_token_lifetime_stale_run_contract.py create mode 100644 tests/test_pr1669_cancel_stale_opencode_runs.py diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..1bb83c2baf --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,163 @@ +# Reusable Dependency Review (workflow_call), consolidating the near- +# identical dependency-review.yml files argos, mightyETL, newsdom-api, +# scopeweave, and naruon each carried independently. See +# docs/adr/0024-dependency-review-reusable-workflow-consolidation.md and +# docs/doctoring/dependency-review-reusable-workflow-consolidation.md for the +# per-repo field audit behind these inputs. +# +# The `on: pull_request` trigger (and any branch restriction) stays in each +# calling repo's own thin workflow file -- a workflow_call target cannot also +# be the thing GitHub triggers directly on pull_request. +# +# Dependency Review requires GitHub Dependency Graph (and, on private repos +# without GitHub Advanced Security, it is unavailable regardless of a repo's +# own settings). scopeweave's original workflow already detected this +# dynamically via the dependency-graph compare API instead of assuming from +# public/private repository status (mightyETL's original approach, which is +# wrong for a private repo that does have GHAS). This reusable workflow +# adopts the dynamic detection as the common, more-correct behavior for +# every caller, so no per-repo public/private input is needed. +# +# Example caller (.github/workflows/dependency-review.yml in a product repo). +# Pin `uses:` to this file's exact commit SHA, not @main: an unpinned mutable +# ref would run an unreviewed central change against every PR check in the +# calling repo (Devin flagged this on the first four callers; fixed in all of +# them). If the calling repo's branch protection requires a status check +# literally named after the old standalone job, converting to `uses:` here +# will rename the published check to " / dependency-review" and +# silently break that required check -- update the branch protection's +# required-check name to match before or immediately after merging a caller. +# +# name: Dependency Review +# on: +# pull_request: +# concurrency: +# group: dependency-review-${{ github.event.pull_request.number || github.ref }} +# cancel-in-progress: true +# jobs: +# dependency-review: +# uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@ +# with: +# fail_on_severity: high +# allow_ghsas: "GHSA-69w3-r845-3855" + +name: Reusable Dependency Review + +on: + workflow_call: + inputs: + fail_on_severity: + description: "Value forwarded to dependency-review-action's fail-on-severity input." + required: false + type: string + default: "moderate" + allow_ghsas: + description: >- + Comma-or-newline-separated GHSA IDs forwarded to + dependency-review-action's allow-ghsas input. Empty (the default) + allows none. + required: false + type: string + default: "" + continue_on_error: + description: >- + Whether the dependency-review step itself is allowed to fail + without failing the job (argos's original behavior, which relies + on a separate blocking OSV-Scanner gate instead of this one). + Default false makes the dependency-review step itself blocking. + required: false + type: boolean + default: false + comment_summary_in_pr: + description: >- + Value forwarded to dependency-review-action's comment-summary-in-pr + input. Default "on-failure" (scopeweave's original choice, applied + uniformly when this input was still hardcoded); naruon explicitly + opts out with "never" -- an explicit per-repo choice, not + accidental drift, so it must stay an input rather than being + flattened to one value. + required: false + type: string + default: "on-failure" + +permissions: + contents: read + pull-requests: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + env: + # Opts every JS action this job runs (checkout, dependency-review-action) + # into the Node 24 actions runtime ahead of GitHub's default cutover, + # matching newsdom-api's original workflow -- applied uniformly here + # since it is a forward-compatibility setting, not a per-repo policy. + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Check dependency graph availability + id: dependency_graph + env: + GH_TOKEN: ${{ github.token }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REPOSITORY: ${{ github.repository }} + shell: bash + run: | + set -euo pipefail + + if [ "${{ github.event_name }}" != "pull_request" ]; then + echo "available=false" >>"$GITHUB_OUTPUT" + echo "Dependency review only runs as a hard gate for pull_request events." + exit 0 + fi + + api_url="${GITHUB_API_URL:-https://api.github.com}" + response_file="$(mktemp)" + status="$( + curl -fsS -o "$response_file" -w '%{http_code}' \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${api_url}/repos/${REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}" \ + || true + )" + + if [ "$status" = "200" ]; then + echo "available=true" >>"$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$status" = "403" ] || [ "$status" = "404" ]; then + echo "::warning::Dependency graph compare returned HTTP ${status} for ${REPOSITORY}; skipping the dependency-review hard gate (GitHub Dependency Graph, or GitHub Advanced Security on a private repository, is unavailable)." + echo "available=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + + echo "::error::Dependency graph availability check failed with HTTP ${status}. This is not a 'graph unavailable' response (403/404) -- treating it as a genuine failure instead of silently skipping the security gate." + cat "$response_file" + exit 1 + + - name: Dependency review + if: steps.dependency_graph.outputs.available == 'true' + continue-on-error: ${{ inputs.continue_on_error }} + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: ${{ inputs.fail_on_severity }} + allow-ghsas: ${{ inputs.allow_ghsas }} + comment-summary-in-pr: ${{ inputs.comment_summary_in_pr }} + + - name: Dependency graph unavailable note + if: steps.dependency_graph.outputs.available != 'true' && github.event_name == 'pull_request' + run: | + echo "Dependency Review requires GitHub Dependency Graph to be enabled for this repository (and, on private repositories, GitHub Advanced Security)." + echo "Other required dependency-vulnerability gates (OSV-Scanner, Scorecard) remain the blocking coverage until Dependency Graph is available here." diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 2c941983f9..30c9e9a517 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -36,6 +36,13 @@ jobs: cancel-closed-pr-runs: if: github.event_name == 'pull_request_target' && github.event.action == 'closed' runs-on: ubuntu-24.04 + # Bound this job well short of GitHub's 360-minute platform default. Its + # only step is a single-repository, status-filtered gh api --paginate + # list-and-cancel sweep (up to 3 passes x 5 statuses), no branch update + # or merge -- lighter than pr-review-merge-scheduler.yml's scan-pr-queue + # job (PR #1702), which got timeout-minutes: 30 for a comparable + # single-repo scan that also dispatches a review and updates a branch. + timeout-minutes: 20 permissions: actions: write contents: read @@ -180,6 +187,22 @@ jobs: noema-review: name: noema-review runs-on: ubuntu-24.04 + # No job-level timeout-minutes here, deliberately. This job's "Prepare + # Noema model verdict" step calls two_phase.py's call_llm synchronously + # via the contextual-orchestrator gateway and blocks on the model's own + # response -- a job-level wall-clock bound here would cap the model's + # reasoning/tool-use time directly, which docs/product-goal-directive.md + # #8 prohibits ("Model timeout은 application·Agent·Gateway 공통 상한 없이 + # 기본 null이다"; "OpenCode·Strix·Noema의 모델당 2시간 이상을 수용한다"). An + # earlier version of this job set timeout-minutes: 210, reasoning it gave + # that step "the same ~180-minute allowance" PR #1707 gave an unrelated + # step -- that reasoning was wrong: #1707's poll_deadline_epoch bounds a + # step that polls GitHub for whether a *separately triggered* review + # process has posted a verdict yet (an async external wait), not a step + # that itself runs the model synchronously. Any fixed cap on a job whose + # body IS the synchronous model call is exactly the fixed inference-time + # cap the policy forbids. See + # docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md. if: >- github.event_name == 'repository_dispatch' || ( diff --git a/.github/workflows/noema-token-lifetime-quality-ci.yml b/.github/workflows/noema-token-lifetime-quality-ci.yml index cfcd722ee4..ef663df16f 100644 --- a/.github/workflows/noema-token-lifetime-quality-ci.yml +++ b/.github/workflows/noema-token-lifetime-quality-ci.yml @@ -8,12 +8,18 @@ on: - tests/test_noema_reviewer_token_lifetime.py - tests/test_noema_two_phase_handoff.py - tests/test_noema_refreshed_app_identity.py + - tests/test_noema_token_lifetime_stale_run_contract.py - docs/doctoring/noema-review-token-lifetime.md - docs/product-technical-gap-baseline.md - CHANGELOG.md - requirements-opencode-review-ci-hashes.txt - .github/workflows/noema-token-lifetime-quality-ci.yml +# Deterministic quality CI: a synchronize supersedes older work for this PR. +concurrency: + group: noema-token-lifetime-quality-${{ github.event.pull_request.base.repo.full_name }}-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: contents: read @@ -36,10 +42,12 @@ jobs: PYTHONPATH=. python3 -m pytest -q \ tests/test_noema_reviewer_token_lifetime.py \ tests/test_noema_two_phase_handoff.py \ - tests/test_noema_refreshed_app_identity.py + tests/test_noema_refreshed_app_identity.py \ + tests/test_noema_token_lifetime_stale_run_contract.py python3 -m compileall -q \ .github/actions/noema-review/two_phase.py \ tests/test_noema_reviewer_token_lifetime.py \ tests/test_noema_two_phase_handoff.py \ - tests/test_noema_refreshed_app_identity.py + tests/test_noema_refreshed_app_identity.py \ + tests/test_noema_token_lifetime_stale_run_contract.py git diff --check diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index a7415cee22..9c2ff1711e 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -466,7 +466,7 @@ jobs: fi if [ "${live_poll_head,,}" != "${HEAD_SHA,,}" ]; then echo "::notice::Pull request head moved while waiting for a current-head OpenCode verdict; retiring superseded Required OpenCode Review poll." - exit 1 + exit 0 fi if [ "$live_poll_state" = "closed" ]; then echo "PR closed while waiting for the current-head OpenCode verdict; the poll is no longer required." diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index 678e8f0014..505384ccfd 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -23,6 +23,22 @@ permissions: jobs: autofix: runs-on: ubuntu-latest + # No job-level timeout-minutes here, deliberately. This job's dominant + # cost is `opencode run` (up to two invocations: the main autofix pass, + # and a base-merge conflict-resolution pass) -- a job-level wall-clock + # bound here would cap the model's own reasoning/tool-use time, which + # docs/product-goal-directive.md #8 prohibits ("Model timeout은 + # application·Agent·Gateway 공통 상한 없이 기본 null이다"; "OpenCode·Strix· + # Noema의 모델당 2시간 이상을 수용한다"). An earlier version of this job set + # timeout-minutes: 25, reasoning it gave the model call "generous room" -- + # that reasoning was wrong: any fixed job-level cap on a job whose body IS + # the synchronous model call terminates the model's work once elapsed, + # which is exactly the fixed inference-time cap the policy forbids, not a + # wall-clock bound on a step that merely waits on a separate async + # verdict (contrast opencode-review.yml's poll_deadline_epoch, which + # bounds a step polling for a verdict prepared by a different process, + # not the model call itself). See + # docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md. env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository }} diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 8c742a10a0..d7e3f5b05a 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -80,6 +80,13 @@ jobs: cancel-superseded-pr-runs: if: github.event_name == 'pull_request_target' && (github.event.action == 'synchronize' || github.event.action == 'closed') runs-on: ubuntu-24.04 + # Bound this gh-api-only cleanup job so a stuck call (rate limit, hung + # `gh api --paginate`) cannot silently occupy a runner for GitHub's + # 360-minute platform default -- exactly the window when a busy PR is + # producing the superseded runs this job exists to retire. Matches + # current-head-run-coalescer.yml's timeout-minutes: 10 for the same + # run-cleanup shape (checkout-free, gh-api-only, no provider inference). + timeout-minutes: 10 # Prefer the established scheduler credential, but let the close event use # its job-scoped token so abandoned scans are cancelled even when that # optional secret is unavailable. This job never checks out PR code. @@ -1020,6 +1027,12 @@ jobs: needs: strix if: ${{ always() && !cancelled() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' }} runs-on: ubuntu-24.04 + # Single-shot OIDC exchange plus a handful of curl/gh api calls, no loop + # or pagination -- same shape as the agent-mention-*-dispatch.yml + # validate-and-forward jobs, which bound at timeout-minutes: 5. Without + # this the job falls back to GitHub's 360-minute platform default on a + # hung network call. + timeout-minutes: 5 permissions: id-token: write statuses: write # Required for downscoped OIDC status publication. diff --git a/CHANGELOG.md b/CHANGELOG.md index 66145dc939..ac1985d86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- **Fail closed before cancelling stale PR workflow runs.** Validate snapshot `headRefOid` and re-read live PR/run identity immediately before destructive cancellation, including OpenCode/Strix dispatch cleanup, so a missing head or concurrent push cannot cancel the sole current-head evidence or trigger a duplicate review. Also ensures every cancellation path (`cancel_stale_pr_runs`, `cancel_stale_opencode_runs`, `_cancel_revalidated_review_run_refs`) treats a run as cancelled only when `force_cancel_workflow_runs` actually reports success, not merely when live revalidation proved it stale -- superseding PR #1712's simpler `force_cancel_workflow_run_refs` wrapper (removed as dead code; its safety guarantee is preserved inline at every call site by this more thorough revalidate-then-cancel design). +- **Cache `active_workflow_runs` for the life of one `pr_review_merge_scheduler.py` + invocation.** `inspect_pr()` calls `cancel_stale_pr_runs()` unconditionally for + every non-draft PR before any eligibility gate, and several other call sites + (`active_review_run_refs`, `dispatch_strix_evidence`'s busy check) ask the + identical unfiltered `(repo, ("queued", "in_progress"))` question again -- + all against the one repository a scheduler invocation ever targets, with zero + caching anywhere in the file. At the default `MAX_PRS=100` this reissued the + same repository-wide, paginated `gh api .../actions/runs` fetch well over a + hundred times per run. `active_workflow_runs` now memoizes its result keyed on + the full `(repo, statuses, event, created, head_sha)` call shape for one + `main()` invocation, with explicit cache invalidation immediately after the + four places that mutate GitHub Actions run state + (`force_cancel_workflow_runs`, `rerun_actions_job`, `dispatch_opencode_review`, + `dispatch_strix_evidence`) so a later read in the same run can never replay a + pre-mutation snapshot. The four pre-existing `ThreadPoolExecutor` sites and the + correctly-sequential per-PR mutation-budget loop are untouched. See + ADR-0022. - **Consolidate the 18 per-repository hourly review-repair caller workflows into one file.** At the repository owner's request ("이런 Workflow는 단일 파일로 통합하라"), replaced `accounting-information-platform-`, `afipc-`, `bandscope-`, `clearfolio-`, diff --git a/docs/adr/0022-scheduler-active-workflow-runs-cache.md b/docs/adr/0022-scheduler-active-workflow-runs-cache.md new file mode 100644 index 0000000000..ca7e36946c --- /dev/null +++ b/docs/adr/0022-scheduler-active-workflow-runs-cache.md @@ -0,0 +1,174 @@ +# ADR-0022: Cache `active_workflow_runs` per scheduler invocation; stay Python + +- **Status:** Accepted +- **Date:** 2026-09-02 +- **Scope:** ContextualWisdomLab/.github `scripts/ci/pr_review_merge_scheduler.py` + (the `scan-pr-queue` job's PR-queue sweep) + +## Context + +`pr_review_merge_scheduler.py` is 5,428 lines and is invoked by `scan-pr-queue` +with `--max-prs "$MAX_PRS"` (workflow_call default `"100"`; +`.github/workflows/pr-review-merge-scheduler.yml`). Its call path is +`main()` → `fetch_open_prs()` (paginated GraphQL, one repository only -- +`fetch_open_prs(repo, max_prs)` takes a single `repo` string, never a set) → +`enrich_rest_mergeable_states()` (already a bounded `ThreadPoolExecutor`) → +a sequential `for pr in prs: inspect_pr(pr)`. That final loop is correctly +sequential by design, not a naive-parallelize target: `inspect_pr` consumes +stateful, order-dependent mutation-budget counters +(`review_dispatch_limit`/`branch_update_limit`, default `1`) that must be +spent in PR order across the whole sweep. + +`concurrent.futures.ThreadPoolExecutor` already exists at four sites -- +`fetch_open_prs_rest` (REST PR-list enrichment), `enrich_rest_mergeable_states` +(per-PR mergeable-state/compare-freshness enrichment), +`resolve_outdated_review_threads` (outdated-thread resolution), and +`force_cancel_workflow_runs` (batched run cancellation) -- so the "naive +sequential loop of independent reads" pattern this investigation went looking +for is already fixed everywhere it occurs for bulk reads. + +The real remaining inefficiency is different in kind: `inspect_pr()` calls +`cancel_stale_pr_runs(repo, pr, dry_run=dry_run)` **unconditionally** for +every non-draft PR, before any eligibility or budget gate. Non-dry-run, that +calls `active_workflow_runs(repo, ("queued", "in_progress"))` -- two +sequential, repository-wide, paginated `gh api repos/{repo}/actions/runs +--paginate --slurp` calls, unfiltered by PR and filtered client-side +afterward. Because the scheduler only ever targets the one repository passed +on its command line, this exact fetch is reissued from scratch for every PR +in the loop, and several other call sites (`active_review_run_refs`, +`dispatch_strix_evidence`'s busy check) ask the identical unfiltered question +again within the same invocation. There was no caching anywhere in the file +(`functools`/`lru_cache` was not even imported). Worst case at the default +`MAX_PRS=100` with mostly non-draft PRs: well over a hundred redundant +sequential `gh api` round-trips per scheduler invocation, each potentially +multi-page, for data that does not change unless the scheduler's own actions +change it. + +No prior ADR discusses this file's language choice (a repository-wide grep +across `docs/adr/*.md` and `docs/*.md` for the scheduler, scheduler +performance, GIL, or Python/Rust turned up nothing). `scripts/ci/` is 50 +files / 27,115 lines, 100% Python, with zero `.rs` files or `Cargo.toml` +anywhere in the repository -- Python-for-CI-glue is this repository's +existing, uniform convention. +`docs/product-technical-gap-baseline.md` §2.2 (Compute plane) scopes +mandatory Rust to math-science/psychometrics computation and CPU-bound hot +paths, and explicitly permits Python/JS for "orchestration/API adapter" +roles -- exactly what this scheduler is: `gh` CLI / GraphQL+REST glue with no +CPU-bound core. `docs/product-goal-directive.md` §6 separately carries a +narrower, already-authorized escape hatch for the concern this investigation +was chartered to check: if a Python web server hits GIL problems, support +multithreading or move to Python 3.14 -- not "rewrite in Rust." The measured +bottleneck here is redundant sequential I/O wait, not CPU/GIL-bound +computation; CPython threads already release the GIL during subprocess and +network I/O, so a Rust rewrite would not remove these round-trips -- only +avoiding the redundant reads does. + +## Decision + +1. **Cache, not a thread pool, for this hot path.** `active_workflow_runs` + now memoizes its result in a module-level dict keyed on the full call + shape `(repo, tuple(statuses), event, created, head_sha)`. This is a + caching fix in the same spirit as "stop repeating a blocking call that + could be done once" -- and is strictly better than thread-pooling the + redundant calls would have been, since caching also cuts GitHub API + rate-limit consumption instead of only wall clock. +2. **Cache lifetime is exactly one scheduler invocation.** + `reset_active_workflow_runs_cache()` clears the dict; `main()` calls it + once at the top of every run, so no state survives across separate + invocations sharing a process (relevant to tests, and to any future + long-lived caller). +3. **Explicit invalidation on every mutation, not a blind full-invocation + cache.** A blind cache is unsafe here: `dispatch_strix_evidence`'s + `busy_refs` check reads `active_workflow_runs` again immediately after + `force_cancel_workflow_run_refs` cancels stale runs for the same + repository, and a later PR's own `cancel_stale_pr_runs` can run after an + earlier PR's dispatch created a new run in the same repository within the + same invocation. Serving a pre-mutation snapshot to either of those reads + would let a just-cancelled run still look "busy," or let a same-invocation + dispatch go undetected by the repository-wide single-concurrency dispatch + guard. `reset_active_workflow_runs_cache()` is therefore called + immediately after the four places that change GitHub Actions run state: + `force_cancel_workflow_runs` (after a cancel), `rerun_actions_job` (after + a rerun), and `dispatch_opencode_review` / `dispatch_strix_evidence` + (after their dispatch `POST`) -- the complete set found by grepping for + every `force-cancel`, `/rerun`, and `/dispatches` call in the file. +4. **The four existing `ThreadPoolExecutor` sites and the sequential per-PR + mutation-budget loop are untouched.** They already convert independent, + read-only bulk lookups to bounded concurrency where that was safe; nothing + with ordering dependencies (merges, branch updates, review dispatches) was + touched, per this organization's standing rule against parallelizing + anything with side effects or ordering dependencies without strong + evidence. +5. **No Rust rewrite.** Per the gap-baseline and goal-directive citations in + Context above: this script's role and evidence do not meet the bar either + document sets for mandatory or motivated Rust. + +## Consequences + +- In the common case -- most PRs carry no stale old-head runs, so + `force_cancel_workflow_runs` is never called with a non-empty `run_ids` and + never invalidates -- the redundant unfiltered `(repo, ("queued", + "in_progress"))` fetches collapse from up to two per PR to two total for + the whole sweep, matching the investigation's own estimate. +- In the pathological case -- every single PR has a stale run to cancel, so + every iteration invalidates -- the cache provides no savings, but also no + regression: behavior degrades gracefully back to exactly today's + call-per-PR pattern, never worse. +- `tests/test_pr_review_merge_scheduler.py`: two existing call-index + assertions (`test_actions_call_gh_with_expected_arguments`, + `test_actions_control_uses_workflow_token_when_mutation_token_is_app`) + shifted because a busy-check read that used to issue two fresh `gh api` + calls is now a cache hit, and were updated (with an inline comment + explaining the shift) rather than the underlying call counts contorted to + preserve the old indices. Four new tests were added: + `test_active_workflow_runs_caches_repeated_identical_calls` (identical + results, one underlying fetch for many repeated calls), + `test_active_workflow_runs_cache_is_faster_than_repeated_fetches` (a + `time.sleep`-delayed fake `gh` proves a genuine wall-clock improvement, not + just fewer assertions), `test_active_workflow_runs_cache_keys_on_full_call_shape` + (distinct repo/statuses/event/created/head_sha combinations never share an + entry), and + `test_force_cancel_workflow_runs_invalidates_active_workflow_runs_cache` + (a cancellation is never masked by a stale pre-cancellation snapshot). A + new autouse fixture clears the cache between every test so the new + module-global state cannot leak across the file's ~250 other tests. +- `coverage run -m pytest tests && coverage report` remains 100% on + `scripts/ci` (`pr_review_merge_scheduler.py`: 2,208 statements / 940 + branches, zero missed); `interrogate` remains 100%. + +## Rejected alternatives + +- **A blind, never-invalidated full-invocation cache.** Rejected as unsafe: + it would let `dispatch_strix_evidence`'s busy check believe a run this same + invocation just cancelled is still occupying the repository's dispatch + capacity, or let one PR's dispatch go invisible to a later PR's read in the + same repository within the same run -- silently breaking the + "repository busy" single-concurrency dispatch guard the code depends on. +- **`functools.lru_cache` decorating `active_workflow_runs` directly.** + Rejected: `lru_cache` hashes its raw arguments before the function body + runs, so a caller passing `statuses` as a list (the parameter's declared + type is `Sequence[str]`, not specifically `tuple`) would raise + `TypeError: unhashable type` where today's implementation tolerates any + iterable. The manual cache normalizes to `tuple(statuses)` for the key + while still iterating the caller's original argument for the actual `gh` + calls. +- **Converting the unconditional `cancel_stale_pr_runs` call, or the per-PR + loop generally, into a `ThreadPoolExecutor` read-parallelization.** + Rejected: the loop is correctly sequential (the mutation-budget counters + must be consumed in PR order), and the actual inefficiency is a *duplicate* + read of identical data across iterations, not independent reads that could + usefully run concurrently. Caching is strictly better for this specific + shape of waste. +- **Rewrite this scheduler, or just its GitHub-API layer, in Rust.** + Rejected under `docs/product-technical-gap-baseline.md` §2.2's scoping + (mandatory Rust is reserved for CPU-bound math-science/psychometrics + compute; Python/JS is explicitly permitted for orchestration/API-adapter + roles) and `docs/product-goal-directive.md` §6's narrower, already-adopted + GIL escape hatch (multithreading or Python 3.14, not a rewrite). The + measured bottleneck is network I/O wait, which CPython already handles by + releasing the GIL during subprocess/socket calls; a Rust rewrite would not + remove the round-trips themselves, only the caching fix does. If a future + profile shows a genuinely CPU-bound hot path inside this file (none is + evidenced today), the removal/migration condition for revisiting this + decision is: a profiler-attributed CPU-bound function, not I/O-bound `gh` + invocation latency, consuming a measurable share of scheduler wall clock. diff --git a/docs/adr/0024-dependency-review-reusable-workflow-consolidation.md b/docs/adr/0024-dependency-review-reusable-workflow-consolidation.md new file mode 100644 index 0000000000..8746db8b23 --- /dev/null +++ b/docs/adr/0024-dependency-review-reusable-workflow-consolidation.md @@ -0,0 +1,167 @@ +# ADR-0024: Consolidate per-repo Dependency Review workflows into one reusable workflow + +- **Status:** Accepted +- **Date:** 2026-09-02 +- **Scope:** `.github/workflows/dependency-review.yml` (new, central, `workflow_call`); + thin callers in `argos`, `mightyETL`, `newsdom-api`, `scopeweave`, `naruon` + (`naruon` added same-day, see "Addendum: naruon" below) + +## Context + +Four repositories each carried an independently hand-written +`dependency-review.yml` running `actions/dependency-review-action` on pull +requests: `argos`, `mightyETL`, `newsdom-api`, `scopeweave`. This is exactly +the drift `docs/CWL-MASTER-CONTEXT.md` §7 and this repo's own +"individual-repository workflow duplication" standardization effort target — +per-repo copies of the same control drift independently and cost bootup time +on every PR run. + +A field-by-field audit of all four files (2026-09-02) found: + +| Field | argos | mightyETL | newsdom-api | scopeweave | naruon | +| --- | --- | --- | --- | --- | --- | +| `fail-on-severity` | `moderate` | `high` | unset (action default `low`) | `moderate` | `moderate` | +| `allow-ghsas` | none | none | `GHSA-69w3-r845-3855` | none | none | +| `comment-summary-in-pr` | unset | unset | unset | `on-failure` | `never` (explicit) | +| step-level `continue-on-error` | `true` | unset (blocking) | unset (blocking) | unset (blocking) | unset (blocking) | +| Dependency Graph availability handling | none (always runs, no fallback) | static `github.event.repository.private` branch to a separate no-op job | none | dynamic API preflight (`dependency-graph/compare` HTTP status): 200 → run the gate, 403/404 → warn and skip, any other status → hard-fail the job | none | +| `step-security/harden-runner` | absent | absent | absent | absent | present (egress audit) | +| trigger scope | `pull_request: branches: [main, developmental]` | `pull_request` (all branches) | `pull_request` (all branches) | `pull_request` + `workflow_dispatch` | `pull_request: branches: [develop, master, release/**]` + `workflow_dispatch` | +| concurrency group | none | `${{ github.workflow }}-${{ github.event.pull_request.number \|\| github.ref }}` | none | `dependency-review-${{ github.event.pull_request.number \|\| github.ref }}` | `dependency-review-${{ github.event.pull_request.number \|\| github.ref }}` | +| `actions/checkout` pin | unpinned `@v4` | n/a (action doesn't need checkout) | SHA `3d3c42e5...` | SHA `9c091bb2...` (v7.0.0) | SHA `3d3c42e5...` (v7.0.1) | +| `dependency-review-action` pin | unpinned `@v4` | SHA `a1d282b3...` (v5.0.0) | SHA `a1d282b3...` | SHA `a1d282b3...` | SHA `a1d282b3...` | +| `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` | unset | unset | `true` | unset | unset | + +Two findings changed the design from a naive copy-paste consolidation: + +1. **Severity and the GHSA allowlist genuinely vary per repo** — these are + real policy differences (newsdom-api carries a documented upstream false + positive it allowlists; mightyETL runs a stricter `high`-only gate), not + accidental drift. They must stay per-caller inputs, not get silently + flattened to one value. +2. **mightyETL's public/private branch is the wrong generalization.** + `github.event.repository.private == false` assumes GHAS availability + tracks repository visibility, but a private repository can have GitHub + Advanced Security enabled (making Dependency Graph available) while a + public repository can still lack Dependency Graph in edge cases. scopeweave's + dynamic preflight — call the dependency-graph compare API directly and + check the HTTP status — checks the actual capability rather than inferring + it, and already existed independently in one of the four originals. This + ADR generalizes scopeweave's approach to all four callers rather than + mightyETL's, and drops the separate no-op fallback job in favor of one job + with a conditional step (the same job either runs the gate or emits the + unavailability note, never both, with no risk of the fallback job being + forgotten when Dependency Graph later becomes available). scopeweave's + preflight also distinguishes a confirmed-unavailable response (403/404 — + warn and skip) from any other unexpected HTTP status (500, an auth + failure, a transient GitHub API problem — hard-fail the job instead of + silently skipping the security gate); the reusable workflow preserves + that exact distinction rather than the simpler "any non-200 means + unavailable" behavior an initial draft of this workflow used, since + collapsing a real failure into "unavailable" would silently drop + coverage instead of surfacing the problem. +3. **`comment-summary-in-pr: on-failure` is a uniformly-beneficial UX + improvement, not a policy choice.** Only scopeweave's original set it + (posts the dependency-review findings as a PR comment when the gate + fails). It changes nothing about pass/fail semantics, only where a + failure's detail is surfaced, so it is hardcoded uniformly rather than + made an input — the other three repositories gain it for free. +4. **`FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` is a forward-compatibility setting, + not a policy choice.** newsdom-api was the only original to set it, + opting its job into GitHub's Node 24 actions runtime ahead of the default + cutover for the JS actions it runs (`actions/checkout`, + `actions/dependency-review-action` — both JS actions in every one of the + four originals). There is no reason the other three repositories should + not also get this ahead of Node 20's eventual end-of-life, so it is + hardcoded uniformly in the reusable workflow's job `env`, not made an + input. + +## Decision + +Add `.github/workflows/dependency-review.yml` to `ContextualWisdomLab/.github` +as a `workflow_call` reusable workflow with three inputs for the +genuinely-varying fields: `fail_on_severity` (string, default `"moderate"`), +`allow_ghsas` (string, default `""`), and `continue_on_error` (boolean, +default `false`, for argos's non-blocking original behavior). The dynamic +Dependency Graph availability check (scopeweave's design) is hardcoded and +uniform for every caller — it is a correctness fix, not a policy choice, so +it does not need to be an input. + +Each of the four repositories keeps a thin caller workflow with its own +`on: pull_request` trigger (including argos's `branches:` restriction, which +cannot live inside a `workflow_call` target), a `concurrency` group (added to +argos and newsdom-api, which lacked one, bringing all four to the same +cancel-in-progress-on-repush posture used elsewhere in the org per the +concurrency-standardization pass this workflow-consolidation effort is part +of), and `with:` values reproducing that repository's original severity and +allowlist exactly. The old hand-written workflow bodies are deleted from each +repository in the same change, per this org's "repository-local copies are +drift sources, not repo-specific contracts" principle +(`README.md` policy summary; this repo's own `CLAUDE.md`). + +## Consequences + +- One place to fix a bug in the dependency-review logic (e.g. the + availability-detection curl call) instead of four. +- Each repository keeps its own severity/allowlist policy explicitly and + visibly in its own thin caller, not hidden in a shared default that could + silently loosen or tighten a repo's actual gate. +- argos and newsdom-api gain the cancel-in-progress concurrency group they + previously lacked, at no cost — a stale run for a superseded push no longer + keeps running or occupying a runner slot. +- `mightyETL`'s previous two-job (public/private) shape becomes one job; the + private-repo fallback note now fires from a live capability check instead + of an assumption, so it no longer misclassifies a private+GHAS-enabled + repository as unsupported, or a public+Dependency-Graph-disabled repository + as supported. +- argos's `unpinned @v4` and `newsdom-api`'s slightly older checkout pin are + both upgraded to the same current, verified pins the reusable workflow + uses, closing that drift too. + +See `docs/doctoring/dependency-review-reusable-workflow-consolidation.md` for +the full per-repo audit and the exact diffs each caller received, including +two post-merge corrections found by Devin's review on the caller PRs: (1) +every caller now pins `uses:` to this file's exact commit SHA rather than +the mutable `@main`, since a mutable central-workflow reference runs +unreviewed against every caller's PR checks; (2) converting a job to +`uses: ` renames its published check-run to a combined +` / ` name, which broke `newsdom-api`'s branch +protection (it required the old standalone name) until that required-check +name was updated to match. + +## Addendum: naruon (2026-09-02, later the same day) + +A peer session's fresh org-wide workflow-duplication survey (63 repos, 255 +workflow files) found a fifth repository, `naruon`, independently carrying +its own `dependency-review.yml` — missed by the original survey this ADR's +consolidation was based on, which never covered `naruon`. Auditing it found +two real, non-cosmetic differences from the four originals above: + +1. **A `step-security/harden-runner` step (egress audit), present in none + of the original four.** Not a per-repo policy — it is a uniformly + beneficial security-hardening practice already standard elsewhere in + this org's own workflows (e.g. `pr-review-autofix.yml`), so it is added + to the reusable workflow itself, as its first step, applying to every + caller including the four already migrated (no caller-side change + needed for this one). +2. **`comment-summary-in-pr: never`, an explicit opt-out**, conflicting + with the earlier decision (see item 3 above) to hardcode + `comment-summary-in-pr: on-failure` uniformly for every caller. That + earlier decision was made when only scopeweave's original set the + field at all, so "hardcode it uniformly" cost no caller its own choice. + naruon proves that assumption wrong: hardcoding it now would silently + overturn an explicit, deliberate choice naruon's original workflow + made. Corrected by making `comment_summary_in_pr` a proper + `workflow_call` input (default `"on-failure"`, preserving current + behavior for the four already-migrated callers with no changes needed + on their side; `naruon`'s caller explicitly sets `"never"`). + +`naruon`'s other fields (`fail-on-severity: moderate`, no `allow-ghsas`, +multi-branch trigger `develop`/`master`/`release/**` plus +`workflow_dispatch`, its own `concurrency` group, job-level `permissions:` +redundant with the workflow-level block, and an informational "Log +dependency review policy" step) either match an existing input, are +caller-side triggers/concurrency untouched by this ADR's design, or (the +informational logging step, and the redundant job-level `permissions:`) +are dropped as they add no policy value the central workflow or the +underlying action doesn't already provide. diff --git a/docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md b/docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md new file mode 100644 index 0000000000..3e80cfb35b --- /dev/null +++ b/docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md @@ -0,0 +1,91 @@ +# Removing job-level timeout-minutes from autofix and noema-review + +## What was wrong + +Earlier the same day, `pr-review-autofix.yml`'s `autofix` job (#1714) and +`noema-review.yml`'s `noema-review` job (#1715) each received a job-level +`timeout-minutes` (25 and 210 respectively) as part of fixing a real, +separate problem: several central `.github` workflow jobs had **no** +`timeout-minutes` at all, so a genuinely stuck job (a hung transport, a +runner fault) could occupy a shared runner for up to GitHub's 360-minute +platform default, contributing to the org-wide Actions capacity incident +documented elsewhere in `docs/product-technical-gap-baseline.md`. + +That fix was correct for jobs whose steps do bookkeeping (cancel stale runs, +publish a status) or that poll for a verdict a *separate* process prepares +(`opencode-review.yml`'s `poll_deadline_epoch`, which bounds a step polling +GitHub for whether a repository-dispatch-triggered review process has posted +a receipt yet -- the model call itself happens in a different workflow, +`opencode-review-dispatch.yml`, which correctly stayed unbounded). + +It was **wrong** for `autofix` and `noema-review`, because in both of those +jobs the model call itself runs synchronously, in-job: + +- `autofix`'s "Run OpenCode review autofix" step runs `opencode run "$(cat + "$prompt_file")" ...` directly and blocks on its output (and a second + `opencode run` for base-merge conflict resolution, later in the same job). +- `noema-review`'s "Prepare Noema model verdict" step runs + `python3 .github/actions/noema-review/two_phase.py ...`, which itself + calls the model (`NOEMA_LLM_API_URL`, `NOEMA_LLM_MODEL=orchestrator/free`) + and blocks until it returns. + +A job-level `timeout-minutes` on either job does not merely bound "how long +this job waits for something external" -- it bounds the model's own +reasoning/tool-use time directly, because the model call is the job's +dominant, synchronous body. That is exactly the fixed inference-time cap +`docs/product-goal-directive.md` #8 prohibits: "Model timeout은 +application·Agent·Gateway 공통 상한 없이 기본 null이다" (no common upper bound +across the application/agent/gateway stack; defaults to null), and "정확성을 +우선하고 OpenCode·Strix·Noema의 모델당 2시간 이상을 수용한다" (prioritize +accuracy; accommodate over two hours per model for OpenCode/Strix/Noema -- +"over two hours" describes a floor on tolerance, not a ceiling to round up +to and hard-code). + +Both original PR descriptions and in-file comments justified the added +timeouts by analogy to `opencode-review.yml`'s `poll_deadline_epoch` fix +(#1707) -- e.g. "gives that step the same ~180-minute allowance PR #1707 set +for its analogous model-wait deadline." That analogy was the actual mistake: +`poll_deadline_epoch` bounds a step that polls for a verdict a *different, +separately triggered* process prepares (an async external wait with no +model call in the bounded step itself); `autofix`'s and `noema-review`'s +jobs are not analogous, because their bounded step **is** the model call. + +## What changed + +- `.github/workflows/pr-review-autofix.yml`: removed `timeout-minutes: 25` + from the `autofix` job. No replacement bound -- the job has no other + timeout mechanism, matching the policy's "기본 null" default. +- `.github/workflows/noema-review.yml`: removed `timeout-minutes: 210` from + the `noema-review` job. `cancel-closed-pr-runs` (pure GitHub API + bookkeeping, no model call) keeps its unrelated `timeout-minutes: 20`. +- `tests/test_pr_review_autofix_writer_security_contract.py`: + `test_autofix_job_has_a_bounded_runtime` (asserted a timeout WAS present, + 5-60 minutes) replaced with `test_autofix_job_has_no_job_level_timeout` + (asserts one is absent). +- `tests/test_noema_orchestrator_workflow_contract.py`: + `test_noema_review_job_has_a_bounded_runtime_above_the_two_hour_model_allowance` + (asserted a timeout WAS present, 120-360 minutes) replaced with + `test_noema_review_job_has_no_job_level_timeout` (asserts one is absent). + `test_cancel_closed_pr_runs_has_a_bounded_runtime` is untouched -- that + job has no model call, so its bound is correct as-is. + +## Why this was caught, and what stayed the same + +Devin's automated review on `ContextualWisdomLab/.github#1661` flagged a +leftover debris file, `scripts/ci/source_fix_pr1715_no_model_job_timeout.py` +-- part of this org's own autonomous self-repair loop, which had correctly +identified this exact bug and was in the middle of fixing it when its +generated PR was reconciled away as apparent "already-served-its-purpose +debris" without checking whether its fix had actually landed. It had not. +This doctoring entry and the accompanying fix restore, by hand (per this +org's "land it as a normal direct fix, not another self-modifying generator +script" convention), the fix that debris script was attempting. + +`opencode-review.yml`'s `poll_deadline_epoch` (#1707), `pr-review-merge-scheduler.yml`'s +`scan-pr-queue` timeout (#1702), and `strix.yml`'s `cancel-superseded-pr-runs` +/ `publish-manual-pr-evidence-status` timeouts (#1713) were all re-checked +against the same question -- "does the bounded job's own step body run the +model synchronously, or does it wait on a separate async actor / do pure +bookkeeping?" -- and confirmed sound: none of them bound a step that itself +runs a model call. `strix.yml`'s main `strix` job (which does run the model) +correctly remains unbounded, as before. diff --git a/docs/doctoring/ci-workflow-duplication-audit-20260902.md b/docs/doctoring/ci-workflow-duplication-audit-20260902.md new file mode 100644 index 0000000000..d82b0589e7 --- /dev/null +++ b/docs/doctoring/ci-workflow-duplication-audit-20260902.md @@ -0,0 +1,130 @@ +# Doctoring record: org-wide CI workflow duplication audit (2026-09-02) + +- **Date:** 2026-09-02 +- **Subject:** the standing user directive "GitHub Actions 파일을 최대한 통합하라" (consolidate GitHub + Actions files as much as possible) had already yielded three genuine consolidations this session: + `hourly-review-repair.yml` (18 per-repository callers → one matrix-based file, ADR-0021), + `r-package-check.yml` (kaefa/nonnest2 R-CMD-check, ADR-0023, #1716), and a reusable + `dependency-review.yml` target built to reconcile mightyETL/newsdom-api/scopeweave's diverging + policies. A prior repo survey (referred to in this session as "the `wynkr83x1` survey") that found + those candidates may have run with a result-count cap, so this audit re-swept the full org for any + further duplication it might have missed. +- **Decision record:** none in `docs/adr/` — this is a negative/confirmatory finding (no new + consolidation to decide), not an architecture decision. +- **PR:** see the PR that carries this commit. +- **Method:** enumerated all 63 non-archived, non-fork `ContextualWisdomLab` repositories via + `gh api orgs/ContextualWisdomLab/repos --paginate`, listed every `.github/workflows/*.yml` file in + each (255 files total, 19 repos with no `.github/workflows` directory at all), grouped by exact + filename, and — for every filename appearing in 2+ repos — fetched and read the **full content** of + every instance, comparing triggers, job topology, permissions, actual commands/tooling, and security + posture. A shared filename was treated as a hypothesis to verify, never as evidence of duplication by + itself, per the explicit caution this session already learned from the `dependency-review.yml` + consolidation (where superficially similar files hid real severity-threshold and allowlist + differences). + +## Result: 19 filename groups checked, 1 real (trivial) duplicate found + +| Filename | Repos checked | Verdict | +|---|---|---| +| `hourly-product-development.yml` | DiagramWeave, EgressWeave, OriginWeave, ThreadWeave, keyverse, noema | NOT_SAFE | +| `hourly-pr-maintenance.yml` | DiagramWeave, EgressWeave, TEPP, ThreadWeave | MIXED — DiagramWeave/ThreadWeave are a genuine duplicate | +| `hourly-product-loop.yml` | disksage, four-pillars, saju-caldav | NOT_SAFE | +| `hourly-nim-product-development.yml` | TEPP, four-pillars | NOT_SAFE | +| `dependency-review.yml` | `.github`, mightyETL, naruon, newsdom-api, scopeweave | NOT_SAFE (see note below) | +| `codeql.yml` | ContextualWisdomLab.github.io, bandscope, fast-mlsirm, keyverse, litellm-patched-proxy, mightyETL, newsdom-api, scopeweave | NOT_SAFE | +| `release.yml` | EgressWeave, ThreadWeave, bandscope, disksage, four-pillars, inkspan, newsdom-api | NOT_SAFE | +| `fuzz.yml` | clearfolio, codec-carver, contextual-orchestrator, linux-cluster-ops, scopeweave, semantic-data-portal, wardnet | NOT_SAFE | +| `tests.yml` | LineageWeave, appguardrail, newsdom-api, semantic-data-portal | NOT_SAFE | +| `ci.yml` | 26 repos (see full evidence in the workflow journal) | NOT_SAFE | +| `security-audit.yml` | aFIPC, bandscope | NOT_SAFE | +| `scorecard.yml` | litellm-patched-proxy, mightyETL | NOT_SAFE | +| `scorecard-analysis.yml` | `.github`, semantic-data-portal, wardnet | NOT_SAFE | +| `sbom.yml` | bandscope, mightyETL | NOT_SAFE | +| `publish-pypi.yml` | appguardrail, fast-mlsirm | NOT_SAFE | +| `bandit.yml` | bandscope, naruon | NOT_SAFE | +| `pr-governance.yml` | linux-cluster-ops, naruon | NOT_SAFE | +| `deploy.yml` | life-os, naruon | NOT_SAFE | +| `app-ci.yml` | gyeot, naruon | NOT_SAFE | + +**Why NOT_SAFE, not just "different repo names":** every NOT_SAFE verdict above is backed by named, +quoted differences in *policy*, not cosmetics — different languages/toolchains (Rust vs Node vs Python +vs Java/Maven vs Java/Gradle), different security postures (SARIF upload present/absent, +`step-security/harden-runner` present/absent, `security-events: write` present/absent), different +trust models (OIDC trusted publishing vs secret-based PyPI auth), different thresholds (Bandit's +target directory and exclusions, Scorecard's `publish_results` toggle, a SARIF-finding suppression +step present in one file and absent in its closest sibling), and different job topology (job counts +from 1 to 7 within a single filename group). The full per-group evidence (concrete quoted lines, +action-pin SHAs, and reasoning) is preserved in this audit's workflow run journal — see Audit trail +below — and is too long to duplicate here without losing readability. + +### The one genuine duplicate: `hourly-pr-maintenance.yml` in DiagramWeave and ThreadWeave + +Byte-for-byte identical except the cron minute offset (`13` vs `11`, a deliberate stagger to avoid +simultaneous org-wide runs) and the wording of one explanatory comment block (same substance, +different phrasing). Same job name, same job permissions, same reusable-workflow pin +(`ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@3f65dbee6672b78802e7d71d49c390f3817bb03b`), +same `workflow_dispatch.inputs.dry_run` block, same concurrency group pattern, same full `with:` tuning +(`max_prs: "20"`, `stale_opencode_minutes: "60"`, `project_flow: "github-flow"`, `base_branch: "main"`, +`merge_mode: "direct_or_auto"`, `enable_auto_merge: true`, and the rest). + +**Not acted on, deliberately.** These are already two ~20-30 line thin callers of a shared reusable +workflow (`pr-review-merge-scheduler.yml`) — the duplication here is in the *configuration values* +(`with:` block), not in any logic that would benefit from a further reusable-workflow layer. Wrapping +an already-thin wrapper in another reusable workflow for two files this small would be the kind of +unrequested abstraction this repo's own conventions warn against. If a third repo adopts the identical +tuning, promoting `max_prs: "20"`/`stale_opencode_minutes: "60"`/`project_flow: "github-flow"` to +`pr-review-merge-scheduler.yml`'s own input defaults (rather than requiring every caller to repeat +them) would be the right-sized fix at that point, not a new wrapper workflow now. + +**TEPP and EgressWeave were checked and are genuinely NOT part of this duplicate**, despite sharing the +filename and calling the same reusable workflow: TEPP passes no `with:` block at all (runs on the +reusable workflow's own defaults — `max_prs` defaults to `"100"` vs the D/T pair's explicit `"20"`, a +5x difference in per-run scan scope; `stale_opencode_minutes` defaults to `"90"` vs `"60"`, a real +redispatch-threshold difference); EgressWeave is structurally different — two jobs instead of one, the +first calling a different reusable workflow entirely (`pr-review-fix-scheduler.yml`, autofix) and the +second running the merge scheduler with `enable_auto_merge: false` / `merge_mode: disabled` (never +merges, only rechecks) versus the D/T pair's `direct_or_auto`/`true`. + +### Discrepancy found: `dependency-review.yml`'s central reusable target exists but no caller has migrated to it yet + +`.github/workflows/dependency-review.yml` is already a `workflow_call` reusable target with inputs +(`fail_on_severity`, `allow_ghsas`, `continue_on_error`) and a dynamic dependency-graph-availability +probe, and its own header comment documents that it was built specifically to reconcile policy +differences found in mightyETL/newsdom-api/scopeweave's original standalone files. However, as of this +audit, **none of the four caller repos checked (mightyETL, naruon, newsdom-api, scopeweave) has +actually switched its own `dependency-review.yml` to `uses:` the central target** — each still carries +a full standalone implementation, and those standalone implementations still genuinely diverge on +severity threshold (`high` vs `moderate` vs unset), dependency-graph-unavailability handling (a static +`private == false` job split vs a dynamic curl probe vs no gating at all), presence of +`step-security/harden-runner` (naruon only), PR trigger branch scoping (naruon only restricts to +`develop`/`master`/`release/**`), and a vulnerability allowlist entry (newsdom-api only). + +This session had understood from another agent's summary that this consolidation was "already merged" +(the central reusable workflow itself). That appears accurate for the central target's own creation, +but the caller-side migration (each of the four repos actually switching to `uses:` it) had not +happened as of this audit. Recorded here rather than silently assumed complete — a follow-up should +either confirm the caller migrations are tracked elsewhere and just not yet landed, or open the four +caller PRs, in each case checking that repo's `branch-protection required_status_checks` for the old +standalone job name first (the SHA-pin and check-run-rename pitfalls already documented in +`docs/adr/0023-r-cmd-check-reusable-workflow-consolidation.md` and PR #1728 apply identically here). + +## Conclusion + +The org's earlier consolidations (hourly-review-repair, R-CMD-check, and the dependency-review reusable +target) already captured the genuinely duplicated CI logic that existed. What remains under shared +filenames is, with one trivial exception, bespoke per-repo automation that happens to share a naming +convention — different languages, different security postures, and different product-specific policy +in nearly every case checked. Further org-wide filename-based searching is unlikely to surface more +candidates; if new duplication emerges, it will more likely come from two repos independently adopting +the *same new pattern* going forward (worth catching at PR-review time) than from an archaeological +sweep of existing files. + +## Audit trail + +- Workflow run `wf_9d141ecd-c03` (13 parallel agents, one per filename cluster or small bundle) — the + full per-group evidence (quoted differing lines, action-pin SHAs) lives in that run's journal. +- `docs/adr/0021-hourly-review-repair-single-file-consolidation.md`, + `docs/adr/0023-r-cmd-check-reusable-workflow-consolidation.md` — the prior genuine consolidations + this audit checked against for completeness. +- `.github/workflows/dependency-review.yml` — the already-built but not-yet-adopted reusable target + discussed above. diff --git a/docs/doctoring/dependency-review-reusable-workflow-consolidation.md b/docs/doctoring/dependency-review-reusable-workflow-consolidation.md new file mode 100644 index 0000000000..eeadf746d7 --- /dev/null +++ b/docs/doctoring/dependency-review-reusable-workflow-consolidation.md @@ -0,0 +1,264 @@ +# Dependency Review reusable workflow consolidation + +## Decision + +`argos`, `mightyETL`, `newsdom-api`, and `scopeweave` each carried an +independently hand-written `.github/workflows/dependency-review.yml` running +`actions/dependency-review-action` on pull requests. All four are replaced by +one new reusable workflow, `.github/workflows/dependency-review.yml` in this +repository, plus a thin `workflow_call` caller left in place of each +repository's own file. See +[ADR-0024](../adr/0024-dependency-review-reusable-workflow-consolidation.md). + +## Field-by-field audit + +Reading all four files' full bodies (not just the job name and action used) +found real, repo-specific policy differences, not accidental copy drift: + +| Field | argos | mightyETL | newsdom-api | scopeweave | naruon | +| --- | --- | --- | --- | --- | --- | +| `fail-on-severity` | `moderate` | `high` | unset → action default `low` | `moderate` | `moderate` | +| `allow-ghsas` | none | none | `GHSA-69w3-r845-3855` | none | none | +| `comment-summary-in-pr` | unset | unset | unset | `on-failure` | `never` (explicit) | +| step `continue-on-error` | `true` | unset (blocking) | unset (blocking) | unset (blocking) | unset (blocking) | +| availability handling | none | static `repository.private` branch to a separate no-op job | none | dynamic `dependency-graph/compare` HTTP-status preflight: 200 → run, 403/404 → warn+skip, other → hard-fail | none | +| `harden-runner` (egress audit) | absent | absent | absent | absent | present | +| trigger | `pull_request: branches: [main, developmental]` | `pull_request` | `pull_request` | `pull_request`, `workflow_dispatch` | `pull_request: branches: [develop, master, release/**]`, `workflow_dispatch` | +| concurrency group | none | workflow+PR/ref group, cancel-in-progress | none | `dependency-review-`+PR/ref group, cancel-in-progress | `dependency-review-`+PR/ref group, cancel-in-progress | +| `actions/checkout` pin | unpinned `@v4` | not used | SHA `3d3c42e5aac5ba805825da76410c181273ba90b1` | SHA `9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0` (v7.0.0) | SHA `3d3c42e5aac5ba805825da76410c181273ba90b1` (v7.0.1) | +| `dependency-review-action` pin | unpinned `@v4` | SHA `a1d282b36b6f3519aa1f3fc636f609c47dddb294` (v5.0.0) | same SHA | same SHA | same SHA | +| `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` | unset | unset | `true` | unset | unset | + +naruon was found later the same day by a peer session's fresh org-wide survey +-- missed by the original 4-repo survey this consolidation started from. See +"Addendum: naruon" below for the two real design changes it required +(`comment_summary_in_pr` becoming an input instead of a hardcoded uniform +value, and adding `harden-runner` uniformly). + +Two decisions this audit drove (see ADR-0024 for the full reasoning): + +1. `fail_on_severity`, `allow_ghsas`, and `continue_on_error` stay per-caller + `workflow_call` inputs — flattening them to one shared value would + silently loosen mightyETL's `high` gate or newsdom-api's documented GHSA + allowlist exception. +2. scopeweave's dynamic Dependency Graph availability preflight (an actual + API capability check) replaces mightyETL's static + `github.event.repository.private` assumption everywhere, because the + assumption is provably wrong in both directions (a private+GHAS repo, or + a public+Dependency-Graph-disabled repo). argos and newsdom-api gain this + safety net for free; they previously had none. +3. `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true` (newsdom-api's original only) + is applied uniformly in the reusable workflow's job `env` rather than + made an input — it opts the job's JS actions (`checkout`, + `dependency-review-action`, present in all four originals) into GitHub's + Node 24 actions runtime ahead of the default cutover, which is a + forward-compatibility setting all four repositories benefit from + identically, not a per-repo policy choice. + +## Mechanism + +`.github/workflows/dependency-review.yml` (this repository) takes four +`workflow_call` inputs (`fail_on_severity`, `allow_ghsas`, +`continue_on_error`, `comment_summary_in_pr`) and always runs the +harden-runner → checkout → availability-preflight → conditional +dependency-review → conditional unavailability-note sequence. +Each calling repository's own thin `.github/workflows/dependency-review.yml` +keeps that repository's original `on:` trigger block (argos keeps its +`branches: [main, developmental]` restriction — a `workflow_call` target +cannot itself be what GitHub triggers on pull_request), gains a +`concurrency` block if it lacked one, and adds one job: +`uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03` +with only that repository's non-default `with:` values. + +### argos caller + +```yaml +name: Dependency Review + +on: + pull_request: + branches: [main, developmental] + +concurrency: + group: dependency-review-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03 + with: + fail_on_severity: moderate + continue_on_error: true +``` + +### mightyETL caller + +```yaml +name: Dependency Review + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03 + with: + fail_on_severity: high +``` + +### newsdom-api caller + +```yaml +name: dependency-review + +on: + pull_request: + +concurrency: + group: dependency-review-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03 + with: + fail_on_severity: low + allow_ghsas: "GHSA-69w3-r845-3855" +``` + +### scopeweave caller + +```yaml +name: Dependency Review + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: dependency-review-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03 + with: + fail_on_severity: moderate +``` + +scopeweave's original supported a `workflow_dispatch` trigger, but its own +job never gated on the event at the job level — it always ran, and its +"Check dependency review support" step early-exited with `supported=false` +for any non-`pull_request` event (the availability check itself needs +`github.event.pull_request.base.sha` / `.head.sha`, which only exist on a +`pull_request` event). The reusable workflow's preflight step carries this +same event-name guard internally, so the caller does not need its own +job-level `if:` to reproduce it — `workflow_dispatch` stays in the trigger +list and the job still runs, harmlessly skipping the gate exactly as the +original did. + +### naruon caller + +```yaml +name: Dependency Review + +on: + pull_request: + branches: + - develop + - master + - "release/**" + workflow_dispatch: + +concurrency: + group: dependency-review-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@ + with: + fail_on_severity: moderate + comment_summary_in_pr: never +``` + +naruon's original also had a job-level `permissions:` block duplicating the +workflow-level one, and an informational "Log dependency review policy" step +that only printed the policy text and base/head refs -- neither is carried +into the caller: the job-level `permissions:` was redundant, and the log +step added no policy value beyond what `actions/dependency-review-action` +itself already reports on failure. + +## Addendum: naruon (2026-09-02, later the same day) + +A peer session's fresh org-wide workflow-duplication survey (63 repos, 255 +workflow files) found `naruon` independently carrying its own +`dependency-review.yml` -- missed by the original 4-repo survey. Auditing it +found two real differences, not cosmetic ones: + +1. **`step-security/harden-runner` (egress audit)**, absent from all four + original callers. Not a per-repo policy choice -- a uniformly beneficial + hardening practice already standard elsewhere in this org (e.g. + `pr-review-autofix.yml`). Added to the reusable workflow itself as its + first step, so every caller (the four already migrated included) gets it + with no caller-side change required. +2. **`comment-summary-in-pr: never`**, an explicit opt-out that directly + conflicts with the earlier decision to hardcode + `comment-summary-in-pr: on-failure` uniformly (made when only scopeweave's + original set the field, so hardcoding it cost no caller its own choice). + Silently applying that hardcoded value to naruon would overturn a + deliberate choice its original workflow made. Fixed by making + `comment_summary_in_pr` a proper `workflow_call` input, default + `"on-failure"` (no change for the four already-migrated callers), + `naruon`'s caller explicitly setting `"never"`. + +## Post-merge corrections (2026-09-02, same day) + +Two real problems surfaced after the four caller PRs opened, both caught +before any of them merged (except argos, fixed retroactively): + +**1. Mutable `@main` reference (Devin, security finding).** The original +callers referenced `uses: .../dependency-review.yml@main` — the example +above now shows the corrected pattern. A mutable branch ref means an +unreviewed change to `.github`'s `main` (or a reference-tampering attack) +runs directly against every caller's PR checks with zero review in the +calling repo. Fixed by pinning every caller to the exact commit SHA that +added the file, `0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03` (unchanged since +it merged) — `argos` retroactively (a follow-up PR after its original +merge), the other three before their first merge. This is now the +documented pattern in the reusable workflow's own header comment: pin +`uses:` to a commit SHA for every caller, the same way every *action* step +inside the reusable workflow itself is already SHA-pinned. + +**2. Required-status-check name collision (Devin, bug finding on +newsdom-api).** Converting a job from inline steps to `uses: ` changes the check-run name GitHub publishes, from the caller +job's own name (e.g. `dependency-review`) to a combined +` / ` (here, +`dependency-review / dependency-review`). `newsdom-api`'s `develop` branch +protection required a status check named literally `dependency-review` — +after conversion, that exact name is never published again, so the +required check stays pending forever and blocks every future merge. +Verified live: `argos` and `mightyETL` have no branch protection at all +(nothing to break); `scopeweave`'s required checks don't include +`dependency-review`; only `newsdom-api` was affected. Fixed by updating +`newsdom-api`'s branch protection required-status-checks list directly +(`gh api -X PATCH repos/.../branches/develop/protection/required_status_checks`), +replacing `dependency-review` with the actual published name +`dependency-review / dependency-review`. This is a general gotcha for any +future "convert a standalone job to a reusable-workflow caller" change — +check the target repo's branch protection for a required check matching the +job's *old* name before or immediately after merging the conversion. + +## Verified before merge + +- `python3 -c "import yaml; yaml.safe_load(open(...))"` on all five files + (the reusable workflow and four callers). +- `actionlint` clean on all five files. +- Full `coverage run -m pytest tests` (2626 passed, 1 skipped) plus + `interrogate` on `ContextualWisdomLab/.github`, confirming the new + contract test and no regression elsewhere. diff --git a/docs/doctoring/noema-token-lifetime-stale-run-retirement.md b/docs/doctoring/noema-token-lifetime-stale-run-retirement.md new file mode 100644 index 0000000000..03de6a5198 --- /dev/null +++ b/docs/doctoring/noema-token-lifetime-stale-run-retirement.md @@ -0,0 +1,39 @@ +# Noema token-lifetime quality stale-run retirement + +## Status + +Proposed on the repair branch pending exact-current-head protected review and Checks. This document is evidence/doctoring, not merge authority. + +## Incident and root cause + +On 2026-09-02, pushing `ContextualWisdomLab/.github#1717` from predecessor head `5b8badc3b9088a5845abc447ed75bf2d9a99031d` to current-main reconciliation head `aeae0c681b66c2e6e9b98d13e47d684eb350b0a8` correctly retired the predecessor runs for Security Scan, OSV-Scanner PR, Semgrep, CodeQL, Strix Changed Path Quality CI, contextual-orchestrator review-repair quality, Python Security, organization commercial readiness, Secret Scan, Scorecard, SBOM, OpenCode Rust coverage, and exact-artifact SBOM quality. The predecessor `Noema Reviewer Token Lifetime CI` run `33621482031`, however, remained queued while the new-head run `33622618082` was also queued. + +The owner workflow `.github/workflows/noema-token-lifetime-quality-ci.yml` had no `concurrency` contract at all. A PR synchronize therefore created a new expensive validation without retiring the obsolete queued/in-progress run for the same repository + PR lineage. This directly violated the control-plane stale-Actions contract and consumed scarce shared Actions capacity. + +## RED → repair contract + +A regression was committed first at `181889f260d3c0f5a048a52f58e470bfb9090b64`. It requires this pull-request workflow to use a repository + PR stable concurrency group, deliberately excludes both `github.event.pull_request.head.sha` and `github.sha`, and requires `cancel-in-progress: true`. The unmodified protected-main workflow fails immediately because it contains no `concurrency:` block. + +The production repair adds only the missing PR-stable concurrency boundary: + +- repository identity: `github.event.pull_request.base.repo.full_name`; +- PR identity: `github.event.pull_request.number`; +- no head SHA in the group; +- `cancel-in-progress: true`. + +This quality gate executes deterministic token-lifetime tests rather than a long semantic reviewer, so preserving superseded in-progress work has no safety benefit. Native GitHub concurrency cancellation is the least-privilege mechanism: it needs no `actions: write`, privileged cancellation token, untrusted-head execution, or custom stale-run API code. + +## Invariants preserved + +The workflow remains `pull_request`-scoped with the same path filter, `contents: read`, `ubuntu-24.04`, exact source checkout, hash-locked CI dependency installation, token-lifetime/two-phase/App-identity pytest targets, compile verification, and `git diff --check`. This change does not alter Noema verdict semantics, contextual-orchestrator routing, provider/model selection, protected branch requirements, or review authority. + +## Live repair-PR evidence + +`ContextualWisdomLab/.github#1726` was opened from repair head `3751cd3b82e48f0131689ab18fbea16ff741f37d`. GitHub admitted `Noema Reviewer Token Lifetime CI` run `33622880158` for that head. This doctoring update intentionally advances the same PR once more so the repaired native concurrency contract can be observed retiring that predecessor run rather than merely asserted from YAML. + +## Verification required before merge + +1. Re-read the exact PR head and workflow text. +2. Prove the regression is GREEN on that exact head. +3. Confirm this synchronize retires predecessor `Noema Reviewer Token Lifetime CI` run `33622880158` and leaves only the current-head authoritative lineage. +4. Re-fetch reviews, unresolved threads, and required/security Checks; merge only through ordinary protection unless the strict independently verified `QUEUE_SATURATION_CHICKEN_EGG` boundary is freshly satisfied. diff --git a/docs/doctoring/scheduler-stale-headrefoid-cancellation.md b/docs/doctoring/scheduler-stale-headrefoid-cancellation.md new file mode 100644 index 0000000000..8f526516e7 --- /dev/null +++ b/docs/doctoring/scheduler-stale-headrefoid-cancellation.md @@ -0,0 +1,46 @@ +# Scheduler stale-head cancellation: fail closed at the destructive boundary + +## Incident + +On 2026-09-02, `ContextualWisdomLab/naruon#1528` had Strix run `33581213829` +cancelled while head `cf472cf77fb93325858f485a22e967449d7c387a` was still the pull +request's sole current head. The run-local Strix supersession job was skipped; +the shared merge scheduler remained a separate cancellation authority. + +## Root cause + +`stale_pr_run_ids()` and `active_review_run_refs()` converted an unresolved or +malformed `headRefOid` into non-authoritative comparison state. Their downstream +destructive paths trusted an earlier snapshot. A push between classification and +cancellation could therefore make a newly current run appear stale. The direct +OpenCode and Strix dispatch paths also cancelled their classified stale refs +without refreshing run and pull-request identity. + +## Repair contract + +- Snapshot heads pass the canonical 40-hex SHA validator. Missing or malformed + heads preserve all active runs. +- Every direct and central-review cancellation candidate is re-read immediately + before its destructive cancellation call. +- The live pull request must still be open, expose an explicit live draft state, and + expose a valid head SHA. Open drafts remain eligible for stale review-run cleanup + because draft review-only dispatch is supported; merge admission stays independently draft-gated. +- The candidate run must still be queued/in-progress and retain the expected + direct PR association or trusted central dispatch target. +- A candidate that now matches the live head, or whose identity/state cannot be + proven, is preserved and blocks duplicate dispatch rather than being cancelled. +- Genuine older-head runs remain cancellable, including the bounded parallel + multi-candidate path. + +This aligns the Python scheduler with the live-reference race contract already +used by `scripts/ci/revalidate_queue_cancellation.sh`. + +## Verification + +The one-shot publisher first installs isolated regressions and requires each one +to finish as exactly one ordinary pytest failure (`exit=1`, `1 failed`) before +production transformation. Collection/environment failures are not accepted as +RED evidence. Final verification runs the focused scheduler suite, complete +repository suite with 100% statement/branch coverage, 100% `scripts/ci` +docstring coverage, compileall, and diff hygiene. The publisher, workflow, and +all temporary repair artifacts delete themselves from the published successor. diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 8c640b0b2d..d8c4ce9b63 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -2677,6 +2677,30 @@ def rerun_actions_job(repo: str, job_id: str, *, dry_run: bool, action: str) -> return require_github_actions_control_actor(action) run_github_actions(["gh", "api", "-X", "POST", f"repos/{repo}/actions/jobs/{job_id}/rerun"]) + # A rerun brings a completed run back to queued/in_progress; invalidate + # any cached active_workflow_runs snapshot so it is not read as stale. + reset_active_workflow_runs_cache() + + +_active_workflow_runs_cache: dict[ + tuple[str, tuple[str, ...], str | None, str | None, str | None], list[dict[str, Any]] +] = {} + + +def reset_active_workflow_runs_cache() -> None: + """Clear the per-invocation cache backing :func:`active_workflow_runs`. + + ``main`` calls this once at the top of every scheduler run so the cache + never survives across separate invocations sharing a process (tests + calling ``main`` more than once, most notably). It must also be called + immediately after anything that changes GitHub Actions run state -- + force-cancelling, rerunning, or dispatching a run -- so a later read in + the same run observes that mutation instead of a stale pre-mutation + snapshot; :func:`force_cancel_workflow_runs`, :func:`rerun_actions_job`, + :func:`dispatch_opencode_review`, and :func:`dispatch_strix_evidence` all + do this immediately after their mutating call. + """ + _active_workflow_runs_cache.clear() def active_workflow_runs( @@ -2699,7 +2723,20 @@ def active_workflow_runs( run history only grows, such as a same-head dispatch search, or one scoped to a single known commit -- should pass them to avoid paginating history it can never use. + + Results are memoized per exact ``(repo, statuses, event, created, + head_sha)`` combination for the life of the cache (cleared by + :func:`reset_active_workflow_runs_cache`). The scheduler's queue sweep + calls the unfiltered ``(repo, ("queued", "in_progress"))`` shape from + every non-draft PR's unconditional stale-run check plus every review + dispatch check, all against the one repository a scheduler invocation + ever targets -- without memoization that is up to two redundant, + repository-wide, paginated REST calls per PR for identical data. """ + cache_key = (repo, tuple(statuses), event, created, head_sha) + cached = _active_workflow_runs_cache.get(cache_key) + if cached is not None: + return list(cached) runs: list[dict[str, Any]] = [] for status in statuses: args = [ @@ -2725,7 +2762,8 @@ def active_workflow_runs( pages = payload if isinstance(payload, list) else [payload] for page in pages: runs.extend(page.get("workflow_runs") or []) - return runs + _active_workflow_runs_cache[cache_key] = runs + return list(runs) def workflow_run_mentions_pr(run_data: dict[str, Any], pr_number: int) -> bool: @@ -2741,7 +2779,15 @@ def stale_pr_run_ids( statuses: Sequence[str] = ("queued", "in_progress"), ) -> list[str]: """Return active run ids for older heads of the same pull request.""" - head = str(pr.get("headRefOid") or "").lower() + raw_head = pr.get("headRefOid") + try: + head = validate_git_sha(str(raw_head or "")).lower() + except (TypeError, ValueError) as exc: + print( + f"::warning::stale_pr_run_ids: PR #{pr.get('number')} in {repo} has an " + f"invalid or unresolved headRefOid; preserving active runs ({exc})." + ) + return [] number = int(pr["number"]) stale: list[str] = [] for run_data in active_workflow_runs(repo, statuses): @@ -2778,7 +2824,15 @@ def active_review_run_refs( centralized_dispatch = bool( (os.environ.get("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY") or "").strip() ) - head = str(pr.get("headRefOid") or "").lower() + raw_head = pr.get("headRefOid") + try: + head = validate_git_sha(str(raw_head or "")).lower() + except (TypeError, ValueError) as exc: + print( + f"::warning::active_review_run_refs: PR #{pr.get('number')} in {target_repo} has an " + f"invalid or unresolved headRefOid; preserving review runs ({exc})." + ) + return [], [] number = int(pr["number"]) dispatch_title_prefixes = tuple( f"{title} {target_repo}#{number}@" @@ -2956,6 +3010,12 @@ def cancel_one(run_id: str) -> tuple[str, str | None]: with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: results = list(executor.map(cancel_one, (str(run_id) for run_id in run_ids))) + # A cancelled run is no longer queued/in_progress; drop any cached + # active_workflow_runs snapshot so the next read (this same PR's later + # checks, or a later PR sharing this repository) sees the change instead + # of replaying it from before the cancellation. + reset_active_workflow_runs_cache() + failures = {run_id: reason for run_id, reason in results if reason is not None} for run_id, reason in failures.items(): print( @@ -2966,33 +3026,146 @@ def cancel_one(run_id: str) -> tuple[str, str | None]: return failures -def force_cancel_workflow_run_refs(run_refs: Sequence[tuple[str, str]]) -> None: - """Force-cancel repository-qualified runs while retaining bounded batches.""" - runs_by_repo: dict[str, list[str]] = {} - for run_repo, run_id in run_refs: - runs_by_repo.setdefault(run_repo, []).append(run_id) - for run_repo, run_ids in runs_by_repo.items(): - force_cancel_workflow_runs(run_repo, run_ids) +def _fresh_open_pr_for_cancellation(repo: str, number: int) -> dict[str, Any]: + """Return fresh open PR authority, including explicitly identified draft state.""" + payload = gh_api_json(f"repos/{repo}/pulls/{number}") + if not isinstance(payload, dict) or str(payload.get("state") or "").lower() != "open": + raise ValueError(f"PR #{number} in {repo} is not a resolvable open pull request") + if payload.get("draft") not in {True, False}: + raise ValueError(f"PR #{number} in {repo} has no authoritative live draft state") + validate_git_sha(str(((payload.get("head") or {}).get("sha")) or "")) + return payload + + +def _fresh_active_run_for_cancellation(run_repo: str, run_id: str) -> dict[str, Any]: + """Return fresh active workflow-run evidence immediately before cancellation.""" + payload = gh_api_json(f"repos/{run_repo}/actions/runs/{run_id}") + if not isinstance(payload, dict) or str(payload.get("status") or "").lower() not in { + "queued", + "in_progress", + }: + raise ValueError(f"workflow run {run_repo}#{run_id} is not active") + return payload + + +def _fresh_pr_head_for_cancellation(repo: str, number: int) -> str: + """Return the validated head SHA from fresh ready/open PR authority.""" + payload = _fresh_open_pr_for_cancellation(repo, number) + return validate_git_sha(str(((payload.get("head") or {}).get("sha")) or "")).lower() + + +def _direct_pr_run_still_superseded(repo: str, number: int, run_id: str) -> bool: + """Return whether a direct PR run is still older than the freshly fetched live head.""" + try: + run_data = _fresh_active_run_for_cancellation(repo, run_id) + if run_data.get("event") == "repository_dispatch" or not workflow_run_mentions_pr( + run_data, number + ): + raise ValueError("workflow run no longer has direct pull-request authority") + run_head = validate_git_sha(str(run_data.get("head_sha") or "")).lower() + live_head = _fresh_pr_head_for_cancellation(repo, number) + except (KeyError, RuntimeError, TypeError, ValueError) as exc: + print( + f"::warning::Preserving workflow run {run_id} in {repo}: " + f"live stale-run revalidation failed closed ({exc})." + ) + return False + return run_head != live_head + + +def _review_run_target_head( + run_data: dict[str, Any], repo: str, workflow: str, number: int +) -> str: + """Return a validated target head for one direct or trusted central review run.""" + if run_data.get("event") == "repository_dispatch": + titles = {"Required OpenCode Review", workflow, *OPENCODE_WORKFLOW_NAMES} + display_title = str(run_data.get("display_title") or "") + prefixes = tuple( + f"{title} {repo}#{number}@" for title in sorted(titles, key=len, reverse=True) + ) + prefix = next((candidate for candidate in prefixes if display_title.startswith(candidate)), None) + if prefix is None: + raise ValueError("repository_dispatch run has no trusted target identity") + return validate_git_sha(display_title.removeprefix(prefix)).lower() + if not workflow_run_mentions_pr(run_data, number): + raise ValueError("review run no longer belongs to the target pull request") + return validate_git_sha(str(run_data.get("head_sha") or "")).lower() + + +def _review_run_still_superseded( + repo: str, + workflow: str, + number: int, + run_repo: str, + run_id: str, +) -> bool: + """Return whether one review run remains stale against fresh ready/open PR authority.""" + try: + run_data = _fresh_active_run_for_cancellation(run_repo, run_id) + run_head = _review_run_target_head(run_data, repo, workflow, number) + live_head = _fresh_pr_head_for_cancellation(repo, number) + except (KeyError, RuntimeError, TypeError, ValueError) as exc: + print( + f"::warning::Preserving review run {run_repo}#{run_id}: " + f"live stale-run revalidation failed closed ({exc})." + ) + return False + return run_head != live_head def cancel_stale_pr_runs(repo: str, pr: dict[str, Any], *, dry_run: bool) -> list[str]: - """Force-cancel queued or running workflows for older heads of the same PR.""" + """Force-cancel only direct-run candidates still proven stale at the destructive boundary.""" if dry_run: return [] require_github_actions_control_actor("force-cancel-stale-pr-runs") - run_ids = stale_pr_run_ids(repo, pr) - force_cancel_workflow_runs(repo, run_ids) - return run_ids + number = int(pr["number"]) + candidates = [str(run_id) for run_id in stale_pr_run_ids(repo, pr)] + + def cancel_one(run_id: str) -> str | None: + """Revalidate and cancel one direct workflow-run candidate when still stale.""" + if not _direct_pr_run_still_superseded(repo, number, run_id): + return None + failures = force_cancel_workflow_runs(repo, [run_id]) + if run_id in failures: + return None + return run_id + + if len(candidates) <= 1: + results = [cancel_one(run_id) for run_id in candidates] + else: + max_workers = min(REST_MERGEABLE_STATE_WORKERS, len(candidates)) + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + results = list(executor.map(cancel_one, candidates)) + return [run_id for run_id in results if run_id is not None] def cancel_stale_opencode_runs(repo: str, workflow: str, pr: dict[str, Any], *, dry_run: bool) -> list[str]: - """Force-cancel older OpenCode runs for the same PR before retrying current head.""" + """Force-cancel only review candidates still proven stale at the destructive boundary.""" if dry_run: return [] require_github_actions_control_actor("force-cancel-stale-opencode-review") + number = int(pr["number"]) _, stale_refs = active_opencode_run_refs(repo, workflow, pr) - force_cancel_workflow_run_refs(stale_refs) - return [run_id for _, run_id in stale_refs] + + def cancel_one(run_ref: tuple[str, str]) -> str | None: + """Revalidate and cancel one review-run candidate when still stale.""" + run_repo, run_id = run_ref + if not _review_run_still_superseded(repo, workflow, number, run_repo, run_id): + return None + failures = force_cancel_workflow_runs(run_repo, [run_id]) + if run_id in failures: + return None + return run_id + + if len(stale_refs) <= 1: + results = [cancel_one(run_ref) for run_ref in stale_refs] + else: + max_workers = min(REST_MERGEABLE_STATE_WORKERS, len(stale_refs)) + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + results = list(executor.map(cancel_one, stale_refs)) + return [run_id for run_id in results if run_id is not None] + + def discover_opencode_required_run_id(repo: str, head_sha: str) -> int | None: @@ -3045,6 +3218,44 @@ def discover_opencode_required_run_id(repo: str, head_sha: str) -> int | None: return newest_id +def _cancel_revalidated_review_run_refs( + repo: str, + workflow: str, + pr: dict[str, Any], + run_refs: list[tuple[str, str]], +) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]: + """Cancel only review refs still proven stale immediately before each destructive call. + + A failed/malformed live read is preservation authority, not permission to + dispatch a duplicate review. The returned first list therefore contains + every active candidate that could not be proven stale; callers fold those + refs into their current/busy set. Multiple candidates retain the scheduler's + existing bounded executor and deterministic input ordering. + """ + if not run_refs: + return [], [] + number = int(pr["number"]) + + def cancel_one(run_ref: tuple[str, str]) -> tuple[str, tuple[str, str]]: + """Revalidate one candidate and cancel it only while it remains stale.""" + run_repo, run_id = run_ref + if not _review_run_still_superseded(repo, workflow, number, run_repo, run_id): + return "preserved", run_ref + failures = force_cancel_workflow_runs(run_repo, [run_id]) + if run_id in failures: + return "preserved", run_ref + return "cancelled", run_ref + + if len(run_refs) == 1: + outcomes = [cancel_one(run_refs[0])] + else: + max_workers = min(REST_MERGEABLE_STATE_WORKERS, len(run_refs)) + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + outcomes = list(executor.map(cancel_one, run_refs)) + preserved = [run_ref for state, run_ref in outcomes if state == "preserved"] + cancelled = [run_ref for state, run_ref in outcomes if state == "cancelled"] + return preserved, cancelled + def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dry_run: bool) -> str: """Dispatch trusted OpenCode for the PR head, or report an active run. @@ -3057,7 +3268,10 @@ def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dr if not dry_run: require_github_actions_control_actor("inspect-active-opencode-review") current_run_refs, stale_run_refs = active_opencode_run_refs(repo, workflow, pr) - force_cancel_workflow_run_refs(stale_run_refs) + preserved_run_refs, _cancelled_run_refs = _cancel_revalidated_review_run_refs( + repo, workflow, pr, stale_run_refs + ) + current_run_refs = [*current_run_refs, *preserved_run_refs] if current_run_refs: print( "OpenCode review dispatch skipped: active same-head workflow run(s) " @@ -3103,6 +3317,9 @@ def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dr } ), ) + # A dispatch queues a new run; invalidate any cached active_workflow_runs + # snapshot so a later busy/current-run check in this same invocation sees it. + reset_active_workflow_runs_cache() return "dispatched" @@ -3131,7 +3348,10 @@ def dispatch_strix_evidence(repo: str, workflow: str, pr: dict[str, Any], *, dry run_title="Strix Security Scan", workflow_aliases=frozenset({"Strix Security Scan"}), ) - force_cancel_workflow_run_refs(stale_run_refs) + preserved_run_refs, cancelled_refs = _cancel_revalidated_review_run_refs( + repo, workflow, pr, stale_run_refs + ) + current_run_refs = [*current_run_refs, *preserved_run_refs] if current_run_refs: print( "Strix evidence dispatch skipped: active same-head workflow run(s) " @@ -3142,12 +3362,12 @@ def dispatch_strix_evidence(repo: str, workflow: str, pr: dict[str, Any], *, dry return "already_running" target_repo = validate_github_repository(repo) dispatch_repo = repository_dispatch_target(target_repo) - stale_ids = {run_id for _, run_id in stale_run_refs} + cancelled_ids = {run_id for _, run_id in cancelled_refs} busy_refs = [ (dispatch_repo, str(run_data["id"])) for run_data in active_workflow_runs(dispatch_repo) if run_data.get("id") - and str(run_data["id"]) not in stale_ids + and str(run_data["id"]) not in cancelled_ids and run_data.get("name") == workflow and run_data.get("event") == "repository_dispatch" and str(run_data.get("display_title") or "").startswith( @@ -3184,6 +3404,9 @@ def dispatch_strix_evidence(repo: str, workflow: str, pr: dict[str, Any], *, dry } ), ) + # A dispatch queues a new run; invalidate any cached active_workflow_runs + # snapshot so a later busy/current-run check in this same invocation sees it. + reset_active_workflow_runs_cache() return "dispatched" @@ -5335,6 +5558,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace: def main(argv: list[str]) -> int: """Run the scheduler CLI.""" + # Each invocation is a fresh look at GitHub; never reuse another + # invocation's active_workflow_runs cache (relevant when a process + # calls main() more than once, tests included). + reset_active_workflow_runs_cache() args = parse_args(argv) if args.self_test: self_test() diff --git a/tests/test_dependency_review_reusable_workflow_contract.py b/tests/test_dependency_review_reusable_workflow_contract.py new file mode 100644 index 0000000000..cadefcb8ac --- /dev/null +++ b/tests/test_dependency_review_reusable_workflow_contract.py @@ -0,0 +1,142 @@ +"""Contract for the reusable Dependency Review workflow. + +Replaces argos's, mightyETL's, newsdom-api's, and scopeweave's +independently hand-written ``dependency-review.yml`` files with one reusable +``workflow_call`` workflow, ``.github/workflows/dependency-review.yml``, plus +a thin caller left in each product repository. See +``docs/doctoring/dependency-review-reusable-workflow-consolidation.md`` and +``docs/adr/0024-dependency-review-reusable-workflow-consolidation.md`` for +why. +""" + +from __future__ import annotations + +from pathlib import Path + +_WORKFLOW = Path(".github/workflows/dependency-review.yml") + +_CHECKOUT_PIN = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" +_DEPENDENCY_REVIEW_PIN = "a1d282b36b6f3519aa1f3fc636f609c47dddb294" + + +def _workflow_text() -> str: + """Read the reusable Dependency Review workflow as UTF-8 text.""" + return _WORKFLOW.read_text(encoding="utf-8") + + +def test_declares_workflow_call_with_four_inputs_and_recorded_defaults() -> None: + """Every genuinely-varying field found while auditing the five originals is an input.""" + workflow = _workflow_text() + assert "on:\n workflow_call:\n inputs:" in workflow + for name in ( + "fail_on_severity:", + "allow_ghsas:", + "continue_on_error:", + "comment_summary_in_pr:", + ): + assert name in workflow + + assert 'default: "moderate"' in workflow + assert 'default: ""' in workflow + assert "default: false" in workflow + assert 'default: "on-failure"' in workflow + + +def test_step_order_is_harden_then_checkout_then_preflight_then_gated_steps() -> None: + """harden-runner -> checkout -> dependency-graph preflight -> conditional gate/note.""" + workflow = _workflow_text() + order = [ + "Harden the runner", + "actions/checkout@", + "Check dependency graph availability", + "Dependency review", + "Dependency graph unavailable note", + ] + positions = [workflow.index(marker) for marker in order] + assert positions == sorted(positions), "steps are out of order" + + +def test_dependency_review_and_note_steps_are_mutually_exclusive_on_availability() -> None: + """The gate and the fallback note must never both run.""" + workflow = _workflow_text() + assert ( + "if: steps.dependency_graph.outputs.available == 'true'\n" + " continue-on-error: ${{ inputs.continue_on_error }}" + in workflow + ) + assert "if: steps.dependency_graph.outputs.available != 'true'" in workflow + + +def test_inputs_are_forwarded_to_the_dependency_review_action() -> None: + """fail_on_severity, allow_ghsas, and comment_summary_in_pr must reach the action untouched.""" + workflow = _workflow_text() + assert "fail-on-severity: ${{ inputs.fail_on_severity }}" in workflow + assert "allow-ghsas: ${{ inputs.allow_ghsas }}" in workflow + assert "comment-summary-in-pr: ${{ inputs.comment_summary_in_pr }}" in workflow + + +def test_harden_runner_audits_egress() -> None: + """naruon's harden-runner step applies uniformly, not only to that one caller.""" + workflow = _workflow_text() + assert "step-security/harden-runner@" in workflow + assert "egress-policy: audit" in workflow + + +def test_action_pins_are_current_and_uniform() -> None: + """checkout and dependency-review-action share one current pin, not per-caller drift.""" + workflow = _workflow_text() + assert f"actions/checkout@{_CHECKOUT_PIN}" in workflow + assert ( + f"actions/dependency-review-action@{_DEPENDENCY_REVIEW_PIN}" in workflow + ) + + +def test_uniform_fields_are_hardcoded_not_parameterized() -> None: + """Fields byte-identical across all four originals stay static, not inputs.""" + workflow = _workflow_text() + assert "permissions:\n contents: read\n pull-requests: read" in workflow + assert "persist-credentials: false" in workflow + + +def test_forces_node24_runtime_for_js_actions() -> None: + """newsdom-api's Node24 opt-in applies uniformly, not only to that one caller.""" + workflow = _workflow_text() + assert "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true" in workflow + + +def test_availability_check_uses_the_dependency_graph_compare_api() -> None: + """The preflight must query the real capability, not infer from repository visibility.""" + workflow = _workflow_text() + assert "dependency-graph/compare" in workflow + assert "github.event.repository.private" not in workflow + + +def test_availability_check_distinguishes_unavailable_from_genuine_failure() -> None: + """403/404 means 'unavailable, skip gracefully'; any other status must hard-fail + the job instead of silently treating a real error the same as unavailability.""" + workflow = _workflow_text() + assert 'if [ "$status" = "403" ] || [ "$status" = "404" ]' in workflow + assert "available=false" in workflow + assert "::error::Dependency graph availability check failed with HTTP" in workflow + assert "exit 1" in workflow + + +def test_availability_check_only_runs_the_gate_for_pull_request_events() -> None: + """A non-pull_request trigger (e.g. workflow_dispatch) must skip the gate, not error, + since base/head SHAs only exist on a pull_request event.""" + workflow = _workflow_text() + assert '"${{ github.event_name }}" != "pull_request"' in workflow + + +def test_dependency_review_comment_summary_defaults_to_on_failure() -> None: + """scopeweave's PR-comment-on-failure UX applies uniformly by default, overridable per caller. + + naruon explicitly overrides it to "never" -- see + test_declares_workflow_call_with_four_inputs_and_recorded_defaults for + the default assertion and test_inputs_are_forwarded_to_the_dependency_review_action + for the forwarding assertion; this test just pins the specific default + value chosen (scopeweave's original, not naruon's or some other value). + """ + workflow = _workflow_text() + assert 'comment_summary_in_pr:\n' in workflow + assert 'default: "on-failure"' in workflow diff --git a/tests/test_noema_orchestrator_workflow_contract.py b/tests/test_noema_orchestrator_workflow_contract.py index 3f6116caf4..4e8b0e32fb 100644 --- a/tests/test_noema_orchestrator_workflow_contract.py +++ b/tests/test_noema_orchestrator_workflow_contract.py @@ -4,6 +4,7 @@ import os import json +import re import shutil import subprocess import textwrap @@ -368,3 +369,59 @@ def test_strix_gateway_default_and_noema_sidecar_fail_closed(tmp_path: Path) -> ) assert noema.returncode == 1 assert "sidecar must be provisioned before Noema LLM review" in noema.stdout + + +def test_cancel_closed_pr_runs_has_a_bounded_runtime() -> None: + """cancel-closed-pr-runs must not fall back to GitHub's 360-minute default. + + Its only step is a single-repository, status-filtered gh api --paginate + list-and-cancel sweep (up to 3 passes x 5 statuses) with no branch update + or merge -- comparable to, or lighter than, pr-review-merge-scheduler.yml's + scan-pr-queue job, which PR #1702 bounded to timeout-minutes: 30 for a + single-repository scan that also dispatches a review and updates a branch. + """ + workflow = workflow_text("noema-review.yml") + job = workflow.split(" cancel-closed-pr-runs:\n", 1)[1].split("\n noema-review:\n", 1)[0] + + match = re.search(r"^ timeout-minutes: (\d+)$", job, flags=re.MULTILINE) + assert match is not None, "cancel-closed-pr-runs must declare a job-level timeout-minutes" + timeout = int(match.group(1)) + assert 1 <= timeout <= 30 + assert timeout < 360 + + +def test_noema_review_job_has_no_job_level_timeout() -> None: + """noema-review must not carry a job-level timeout-minutes. + + Its "Prepare Noema model verdict" step calls two_phase.py's call_llm + synchronously via the contextual-orchestrator gateway and blocks on the + model's own response -- a job-level wall-clock bound here directly caps + the model's reasoning/tool-use time once elapsed, which + docs/product-goal-directive.md #8 prohibits ("Model timeout은 + application·Agent·Gateway 공통 상한 없이 기본 null이다"). An earlier + version of this job set timeout-minutes: 210, reasoning it gave that + step "the same ~180-minute allowance" opencode-review.yml's + poll_deadline_epoch gives an unrelated step -- that reasoning was + itself the mistake: poll_deadline_epoch bounds a step that polls GitHub + for whether a *separately triggered* review process has posted a + verdict yet (an async external wait), not a step that itself runs the + model synchronously. Any fixed cap on a job whose body IS the + synchronous model call is exactly the forbidden inference-time cap. See + docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md. + """ + workflow = workflow_text("noema-review.yml") + job = workflow.split(" noema-review:\n", 1)[1] + + match = re.search(r"^ timeout-minutes: (\d+)$", job, flags=re.MULTILINE) + assert match is None, ( + "noema-review must not declare a job-level timeout-minutes -- its " + "body is a synchronous model call, so any job-level bound caps " + "model inference time, which this org's model-timeout policy forbids" + ) + + assert ( + "모델당 두 시간 이상 걸릴 수 있음을 수용한다" + in (Path(__file__).resolve().parents[1] / "docs" / "product-goal-directive.md").read_text( + encoding="utf-8" + ) + ), "the two-hour-per-model allowance this bound relies on must still be documented" diff --git a/tests/test_noema_token_lifetime_stale_run_contract.py b/tests/test_noema_token_lifetime_stale_run_contract.py new file mode 100644 index 0000000000..8ff74006ec --- /dev/null +++ b/tests/test_noema_token_lifetime_stale_run_contract.py @@ -0,0 +1,22 @@ +"""Regression contract for Noema token-lifetime PR run retirement.""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "noema-token-lifetime-quality-ci.yml" + + +def test_noema_token_lifetime_quality_ci_retires_superseded_pr_runs() -> None: + """Keep one authoritative PR/head lineage for the token-lifetime quality gate.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + + assert "concurrency:" in workflow + concurrency_contract = workflow.split("concurrency:", 1)[1].split( + "permissions:", 1 + )[0] + assert "github.event.pull_request.base.repo.full_name" in concurrency_contract + assert "github.event.pull_request.number" in concurrency_contract + assert "github.event.pull_request.head.sha" not in concurrency_contract + assert "github.sha" not in concurrency_contract + assert "cancel-in-progress: true" in concurrency_contract diff --git a/tests/test_opencode_poll_self_retirement.py b/tests/test_opencode_poll_self_retirement.py index 17d5e937ae..5a31c39da7 100644 --- a/tests/test_opencode_poll_self_retirement.py +++ b/tests/test_opencode_poll_self_retirement.py @@ -204,7 +204,7 @@ def test_poll_live_state_revalidation_fails_closed_on_malformed_evidence() -> No def test_poll_executes_superseded_head_retirement_before_reviews_read( tmp_path: Path, ) -> None: - """A moved head exits non-passing before the Reviews API is consulted.""" + """A moved head exits successfully before the Reviews API is consulted.""" head_sha = "a" * 40 result, calls = _run_poll_loop( tmp_path, @@ -212,7 +212,7 @@ def test_poll_executes_superseded_head_retirement_before_reviews_read( live_pr={"head": {"sha": "b" * 40}, "draft": False, "state": "open"}, ) - assert result.returncode == 1 + assert result.returncode == 0 assert "retiring superseded Required OpenCode Review poll" in result.stdout assert calls == ["api repos/ContextualWisdomLab/example/pulls/42"] @@ -393,7 +393,6 @@ def test_poll_fails_closed_after_wall_clock_deadline_with_every_gh_call_succeedi "::error::No current-head OpenCode verdict after 180 minutes of " "polling; failing closed and releasing the runner." in result.stdout ) - # Distinct diagnostic from the transport-failure path: nothing here failed. assert "consecutive times" not in result.stdout assert calls == [ "api repos/ContextualWisdomLab/example/pulls/42", @@ -420,7 +419,7 @@ def test_poll_wall_clock_deadline_does_not_interfere_with_a_fast_verdict( "body": "Source-backed current-head semantic review.", } ], - date_epochs=[1000, 1000], # baseline call, then one in-bounds iteration check + date_epochs=[1000, 1000], ) assert result.returncode == 0, result.stderr @@ -453,8 +452,6 @@ def test_wall_clock_deadline_is_distinct_from_and_additional_to_transport_counte "::error::No current-head OpenCode verdict after 180 minutes of " "polling; failing closed and releasing the runner." in loop ) - # The deadline check must precede this iteration's gh calls so an - # already-expired deadline never spends another API request. assert loop.index('-ge "$poll_deadline_epoch"') < loop.index( 'live_poll_pr="$(timeout 30s gh api' ) diff --git a/tests/test_pr1669_cancel_stale_opencode_runs.py b/tests/test_pr1669_cancel_stale_opencode_runs.py new file mode 100644 index 0000000000..9529b87f43 --- /dev/null +++ b/tests/test_pr1669_cancel_stale_opencode_runs.py @@ -0,0 +1,154 @@ +"""Permanent regression coverage for PR #1669's headRefOid cancellation bug. + +Reproduces the live ``ContextualWisdomLab/naruon#1528`` incident: Strix run +``33581213829`` for head ``cf472cf77fb93325858f485a22e967449d7c387a`` was +force-cancelled while it was the PR's sole, unchanged current head, because +``stale_pr_run_ids()`` and ``active_review_run_refs()`` computed the expected +head as ``str(pr.get("headRefOid") or "").lower()`` -- a missing/falsy +``headRefOid`` silently coerced to ``""``, which never equals a real 40-hex +``head_sha``, so every active run for the PR (including the true current-head +run) was misclassified as stale. See +``docs/doctoring/scheduler-stale-headrefoid-cancellation.md``. +""" + +from scripts.ci import pr_review_merge_scheduler as sched + +NARUON_REPO = "ContextualWisdomLab/naruon" +NARUON_PR_NUMBER = 1528 +NARUON_RUN_ID = 33581213829 +NARUON_HEAD_SHA = "cf472cf77fb93325858f485a22e967449d7c387a" + + +def test_stale_pr_run_ids_preserves_current_head_run_when_head_ref_oid_missing(monkeypatch): + """A missing headRefOid must not classify the live current-head run stale.""" + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda *_args, **_kwargs: [ + { + "id": NARUON_RUN_ID, + "head_sha": NARUON_HEAD_SHA, + "pull_requests": [{"number": NARUON_PR_NUMBER}], + } + ], + ) + + stale = sched.stale_pr_run_ids( + NARUON_REPO, {"number": NARUON_PR_NUMBER, "headRefOid": None} + ) + + assert stale == [] + + +def test_active_review_run_refs_preserves_current_head_run_when_head_ref_oid_missing( + monkeypatch, +): + """A missing headRefOid must not classify the live current-head review run stale.""" + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda *_args, **_kwargs: [ + { + "id": NARUON_RUN_ID, + "event": "pull_request", + "name": "Strix Security Scan", + "head_sha": NARUON_HEAD_SHA, + "pull_requests": [{"number": NARUON_PR_NUMBER}], + } + ], + ) + + current, stale = sched.active_review_run_refs( + NARUON_REPO, + "Strix Security Scan", + {"number": NARUON_PR_NUMBER, "headRefOid": None}, + run_title="Strix Security Scan", + workflow_aliases=frozenset({"Strix Security Scan"}), + ) + + assert current == [] + assert stale == [] + + +def test_cancel_stale_pr_runs_issues_no_cancel_call_when_head_ref_oid_missing(monkeypatch): + """A missing headRefOid must yield no stale candidate before the second, + live-revalidation safety net ever runs -- isolated here (by forcing that + net to say "still superseded") so this test depends only on the + ``stale_pr_run_ids`` guard under test, not on the independent live re-fetch.""" + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda *_args, **_kwargs: [ + { + "id": NARUON_RUN_ID, + "head_sha": NARUON_HEAD_SHA, + "pull_requests": [{"number": NARUON_PR_NUMBER}], + } + ], + ) + monkeypatch.setattr(sched, "_direct_pr_run_still_superseded", lambda *_a, **_k: True) + cancelled = [] + monkeypatch.setattr( + sched, + "force_cancel_workflow_runs", + lambda *args: cancelled.append(args), + ) + + run_ids = sched.cancel_stale_pr_runs( + NARUON_REPO, + {"number": NARUON_PR_NUMBER, "headRefOid": None}, + dry_run=False, + ) + + assert run_ids == [] + assert cancelled == [] + + +def test_cancel_stale_opencode_runs_uses_revalidated_refs(monkeypatch): + """Revalidate every candidate and cancel only refs still proven stale.""" + actor_calls: list[str] = [] + revalidated: list[tuple[str, str, int, str, str]] = [] + cancelled: list[tuple[str, list[str]]] = [] + stale_refs = [("owner/repo", "101"), ("owner/repo", "202")] + + monkeypatch.setattr( + sched, + "require_github_actions_control_actor", + lambda action: actor_calls.append(action), + ) + monkeypatch.setattr( + sched, + "active_opencode_run_refs", + lambda _repo, _workflow, _pr: ([], stale_refs), + ) + + def still_superseded(repo, workflow, number, run_repo, run_id): + revalidated.append((repo, workflow, number, run_repo, run_id)) + return True + + monkeypatch.setattr(sched, "_review_run_still_superseded", still_superseded) + + def cancel(repo, run_ids): + cancelled.append((repo, list(run_ids))) + return {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + + run_ids = sched.cancel_stale_opencode_runs( + "owner/repo", + "OpenCode Review", + {"number": 7, "headRefOid": "a" * 40}, + dry_run=False, + ) + + assert actor_calls == ["force-cancel-stale-opencode-review"] + assert sorted(revalidated) == [ + ("owner/repo", "OpenCode Review", 7, "owner/repo", "101"), + ("owner/repo", "OpenCode Review", 7, "owner/repo", "202"), + ] + assert sorted(cancelled) == [ + ("owner/repo", ["101"]), + ("owner/repo", ["202"]), + ] + assert sorted(run_ids) == ["101", "202"] diff --git a/tests/test_pr_review_autofix_writer_security_contract.py b/tests/test_pr_review_autofix_writer_security_contract.py index ca0cc130bb..3f6119424f 100644 --- a/tests/test_pr_review_autofix_writer_security_contract.py +++ b/tests/test_pr_review_autofix_writer_security_contract.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from pathlib import Path @@ -93,3 +94,33 @@ def test_read_only_steps_do_not_prefer_mutation_credentials() -> None: assert "steps.target_app_token.outputs.token || github.token" in header assert "PR_REVIEW_MERGE_TOKEN" not in header assert "OPENCODE_APPROVE_TOKEN" not in header + + +def test_autofix_job_has_no_job_level_timeout() -> None: + """The autofix job must not carry a job-level timeout-minutes. + + This job's body IS a synchronous `opencode run` call (up to two + invocations: the main autofix pass and a base-merge conflict-resolution + pass) -- a job-level wall-clock bound here directly caps the model's own + reasoning/tool-use time once elapsed, which + docs/product-goal-directive.md #8 prohibits ("Model timeout은 + application·Agent·Gateway 공통 상한 없이 기본 null이다"). An earlier version + of this job set timeout-minutes: 25, reasoning it gave the model call + "generous room" -- that reasoning was itself the mistake: any fixed cap + on a job whose body is the model call is exactly the forbidden + inference-time cap, not a bound on a step that merely waits on a + separate async verdict (contrast opencode-review.yml's + poll_deadline_epoch, which bounds a step polling GitHub for a verdict a + *different* process prepares, not the model call itself). See + docs/doctoring/autofix-and-noema-review-model-job-timeout-removal.md. + """ + workflow = _workflow_text() + job = workflow.split(" autofix:\n", maxsplit=1)[1] + job_header = job.split(" steps:\n", maxsplit=1)[0] + + match = re.search(r"^ timeout-minutes: (\d+)$", job_header, flags=re.MULTILINE) + assert match is None, ( + "autofix must not declare a job-level timeout-minutes -- its body is " + "a synchronous model call, so any job-level bound caps model " + "inference time, which this org's model-timeout policy forbids" + ) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index d859b1730d..8b5ddfcbce 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1,6 +1,7 @@ import json import os import sys +import time from datetime import datetime, timezone import pytest @@ -35,6 +36,19 @@ def workflow_starting_mutation_credential(monkeypatch): monkeypatch.setenv("SCHEDULER_MUTATION_TOKEN_SOURCE", "PR_REVIEW_MERGE_TOKEN") +@pytest.fixture(autouse=True) +def reset_active_workflow_runs_cache(): + """Isolate ``active_workflow_runs``'s cache so tests never see a sibling's data. + + Different tests reuse the same ``owner/repo`` cache key with different + fake GitHub responses; without this the module-global cache from one test + would leak into the next. + """ + sched.reset_active_workflow_runs_cache() + yield + sched.reset_active_workflow_runs_cache() + + def fake_github_token(prefix, body): return f"{prefix}{TOKEN_SEPARATOR}{body}" @@ -1410,6 +1424,7 @@ def map(self, func, items): def test_cancel_stale_opencode_runs_uses_bounded_executor_for_multiple_runs(monkeypatch): + monkeypatch.setattr(sched, "_review_run_still_superseded", lambda *_args: True) seen_workers = [] class FakeExecutor: @@ -1486,6 +1501,59 @@ def maybe_fail(args): } +def test_cancel_revalidated_review_run_refs_preserves_failed_cancellation(monkeypatch): + """Keep a review ref busy when GitHub rejects its destructive cancellation. + + Discovered mid-flight during PR #1669's development (the naruon headRefOid + incident fix) and intentionally scoped out of that PR; landing fresh here per + docs/doctoring/scheduler-stale-headrefoid-cancellation.md. The live-revalidating + ``_cancel_revalidated_review_run_refs`` (used by both ``dispatch_opencode_review`` + and ``dispatch_strix_evidence``) must not report a ref as cancelled when the + underlying ``force_cancel_workflow_runs`` call itself was rejected by GitHub, + even though the ref was independently proven still-stale by live revalidation. + """ + stale_refs = [("owner/repo", "101"), ("owner/repo", "202")] + monkeypatch.setattr(sched, "_review_run_still_superseded", lambda *_args: True) + + def cancel(_repo, run_ids): + run_id = str(run_ids[0]) + return {run_id: "GitHub rejected cancellation"} if run_id == "101" else {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + + preserved, cancelled = sched._cancel_revalidated_review_run_refs( + "owner/repo", "OpenCode Review", make_pr(), stale_refs + ) + + assert ("owner/repo", "101") in preserved + assert ("owner/repo", "101") not in cancelled + assert ("owner/repo", "202") in cancelled + + +def test_cancel_stale_opencode_runs_preserves_failed_cancellation(monkeypatch): + """Keep a stale review active when GitHub rejects its cancellation.""" + stale_refs = [("owner/repo", "101"), ("owner/repo", "202")] + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr( + sched, + "active_opencode_run_refs", + lambda _repo, _workflow, _pr: ([], stale_refs), + ) + monkeypatch.setattr(sched, "_review_run_still_superseded", lambda *_args: True) + + def cancel(_repo, run_ids): + run_id = str(run_ids[0]) + return {run_id: "GitHub rejected cancellation"} if run_id == "101" else {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + + run_ids = sched.cancel_stale_opencode_runs( + "owner/repo", "OpenCode Review", make_pr(), dry_run=False + ) + + assert run_ids == ["202"] + + def test_cancel_stale_opencode_runs_dry_run_skips_lookup_and_mutation(monkeypatch): calls = [] monkeypatch.setattr(sched, "stale_opencode_run_ids", lambda *args: calls.append(args) or ["1"]) @@ -1910,7 +1978,6 @@ def test_dispatch_opencode_review_falls_back_to_bounded_discovery(monkeypatch): monkeypatch.setattr( sched, "active_opencode_run_refs", lambda repo, workflow, pr: ([], []) ) - monkeypatch.setattr(sched, "force_cancel_workflow_run_refs", lambda refs: None) monkeypatch.setattr( sched, "discover_opencode_required_run_id", @@ -4160,7 +4227,11 @@ def fake_run(args, stdin=None): assert calls[3][-1] == f"expected_head_sha={head_sha}" assert calls[4][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] assert calls[5][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] - assert calls[8] == [ + # dispatch_strix_evidence's busy_refs check re-reads the exact same + # (repo, ("queued", "in_progress")) shape calls[4:6] already fetched; + # active_workflow_runs's per-invocation cache serves it without a + # third/fourth GET, so its dispatch POST lands right after calls[4:6]. + assert calls[6] == [ "gh", "api", "-X", @@ -4169,17 +4240,20 @@ def fake_run(args, stdin=None): "--input", "-", ] - assert calls[9][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] - assert calls[10][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] - # calls[11:14]: the bounded discover_opencode_required_run_id fallback + # That dispatch invalidates the cache (it just queued a new run), so + # dispatch_opencode_review's own active_opencode_run_refs check below + # re-fetches fresh instead of reusing calls[4:6]'s now-stale snapshot. + assert calls[7][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] + assert calls[8][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] + # calls[9:12]: the bounded discover_opencode_required_run_id fallback # (matching_actions_run_id found nothing in this PR's empty rollup). for offset, status in enumerate(("queued", "in_progress", "completed")): - discover_call = calls[11 + offset] + discover_call = calls[9 + offset] assert discover_call[:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] assert f"status={status}" in discover_call assert "event=pull_request_target" in discover_call assert f"head_sha={head_sha}" in discover_call - assert calls[14] == [ + assert calls[12] == [ "gh", "api", "-X", @@ -4423,7 +4497,11 @@ def fake_run_with_env(args, *, stdin=None, env=None): assert calls[0][0] == ["gh", "api", "-X", "POST", "repos/owner/repo/actions/jobs/101/rerun"] assert calls[1][0][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] assert calls[2][0][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] - assert calls[5][0] == [ + # dispatch_strix_evidence's busy_refs check re-reads the exact same + # (repo, ("queued", "in_progress")) shape calls[1:3] already fetched; + # active_workflow_runs's per-invocation cache serves it without a + # third/fourth GET, so its dispatch POST lands right after calls[1:3]. + assert calls[3][0] == [ "gh", "api", "-X", @@ -4432,19 +4510,22 @@ def fake_run_with_env(args, *, stdin=None, env=None): "--input", "-", ] - assert calls[6][0][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] - assert calls[7][0][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] - # calls[8:11]: the bounded discover_opencode_required_run_id fallback + # That dispatch invalidates the cache (it just queued a new run), so + # dispatch_opencode_review's own active_opencode_run_refs check below + # re-fetches fresh instead of reusing calls[1:3]'s now-stale snapshot. + assert calls[4][0][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] + assert calls[5][0][:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] + # calls[6:9]: the bounded discover_opencode_required_run_id fallback # (matching_actions_run_id found nothing in the empty rollup), scoped to # the exact head SHA across the three statuses that can hold the # required run. for offset, status in enumerate(("queued", "in_progress", "completed")): - discover_call = calls[8 + offset][0] + discover_call = calls[6 + offset][0] assert discover_call[:5] == ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs"] assert f"status={status}" in discover_call assert "event=pull_request_target" in discover_call assert f"head_sha={'a' * 40}" in discover_call - assert calls[11][0] == [ + assert calls[9][0] == [ "gh", "api", "-X", @@ -4709,6 +4790,7 @@ def fake_run(args, stdin=None): def test_dispatch_opencode_review_force_cancels_same_pr_old_head_runs(monkeypatch): + monkeypatch.setattr(sched, "_review_run_still_superseded", lambda *_args: True) calls = [] head_sha = "a" * 40 base_sha = "b" * 40 @@ -5095,7 +5177,123 @@ def fake_run(args, stdin=None): assert not any(str(arg).startswith("created=") for arg in args) +def test_active_workflow_runs_caches_repeated_identical_calls(monkeypatch): + """A repeated identical call is served from cache with the identical result. + + This is the scan-pr-queue win: every non-draft PR unconditionally asks + for the same (repo, ("queued", "in_progress")) shape via + ``cancel_stale_pr_runs``, and review dispatch re-asks the same shape + again -- all against the one repository a scheduler invocation ever + targets. Only the first call should reach the (faked) GitHub API; every + later call with the same arguments must return the same data without a + new call. + """ + calls = [] + + def fake_run(args, stdin=None): + del stdin + calls.append(args) + return json.dumps([{"workflow_runs": [{"id": 1}, {"id": 2}]}]) + + monkeypatch.setattr(sched, "run_github_actions", fake_run) + + first = sched.active_workflow_runs("owner/repo", ("queued", "in_progress")) + for _ in range(50): + repeated = sched.active_workflow_runs("owner/repo", ("queued", "in_progress")) + assert repeated == first + + # 2 calls total: one per status in the first, cache-populating call -- + # not 2 * 51 for 51 identical requests. + assert len(calls) == 2 + + +def test_active_workflow_runs_cache_is_faster_than_repeated_fetches(monkeypatch): + """Caching turns N redundant slow fetches into 1: wall clock reflects that.""" + delay = 0.02 + call_count = 0 + + def slow_fake_run(args, stdin=None): + del args, stdin + nonlocal call_count + call_count += 1 + time.sleep(delay) + return json.dumps([{"workflow_runs": []}]) + + monkeypatch.setattr(sched, "run_github_actions", slow_fake_run) + + repeats = 20 + start = time.monotonic() + for _ in range(repeats): + sched.active_workflow_runs("owner/repo", ("queued", "in_progress")) + elapsed = time.monotonic() - start + + # Uncached, 20 repeats * 2 statuses * 0.02s would take >= 0.8s; cached, + # only the first call's 2 statuses ever sleep. Generous bound keeps this + # robust on a loaded CI runner while still catching a caching regression. + assert call_count == 2 + assert elapsed < delay * 2 * repeats / 2 + + +def test_active_workflow_runs_cache_keys_on_full_call_shape(monkeypatch): + """Distinct repo/statuses/event/created/head_sha never share a cache entry.""" + calls = [] + + def fake_run(args, stdin=None): + del stdin + calls.append(args) + return json.dumps([{"workflow_runs": []}]) + + monkeypatch.setattr(sched, "run_github_actions", fake_run) + + sched.active_workflow_runs("owner/repo", ("queued",)) + sched.active_workflow_runs("owner/other-repo", ("queued",)) + sched.active_workflow_runs("owner/repo", ("in_progress",)) + sched.active_workflow_runs("owner/repo", ("queued",), event="repository_dispatch") + sched.active_workflow_runs("owner/repo", ("queued",), head_sha="a" * 40) + sched.active_workflow_runs("owner/repo", ("queued",)) # repeat of the first: cache hit + + assert len(calls) == 5 + + +def test_force_cancel_workflow_runs_invalidates_active_workflow_runs_cache(monkeypatch): + """A cancellation must not be masked by a stale pre-cancellation cache entry. + + ``dispatch_strix_evidence``'s busy_refs check runs right after + ``force_cancel_workflow_run_refs`` cancels stale runs for the same + repository; if the cache were not invalidated, that check could see a + run this very call just cancelled and wrongly report the repository + busy, or a later PR's cancel_stale_pr_runs could miss a run it should + force-cancel because a same-shape read from before an earlier + cancellation was replayed instead of re-fetched. + """ + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("GH_TOKEN", "workflow-token") + responses = [ + json.dumps([{"workflow_runs": [{"id": 9001}]}]), # queued, before cancel + json.dumps([{"workflow_runs": []}]), # in_progress, before cancel + "", # the force-cancel POST itself + json.dumps([{"workflow_runs": []}]), # queued, after cancel: must re-fetch + json.dumps([{"workflow_runs": []}]), # in_progress, after cancel + ] + + def fake_run(args, stdin=None): + del args, stdin + return responses.pop(0) + + monkeypatch.setattr(sched, "run", fake_run) + + before = sched.active_workflow_runs("owner/repo", ("queued", "in_progress")) + assert before == [{"id": 9001}] + + sched.force_cancel_workflow_runs("owner/repo", ["9001"]) + + after = sched.active_workflow_runs("owner/repo", ("queued", "in_progress")) + assert after == [] + assert responses == [] # every canned response was consumed: no call was skipped or reused + + def test_dispatch_strix_cancels_stale_central_run_and_keeps_current(monkeypatch, capsys): + monkeypatch.setattr(sched, "_review_run_still_superseded", lambda *_args: True) calls = [] head_sha = "a" * 40 stale_sha = "c" * 40 @@ -5323,6 +5521,7 @@ def test_active_run_filters_and_stale_opencode_dry_run(monkeypatch): def test_cancel_stale_pr_runs_force_cancels_queued_and_in_progress_old_heads(monkeypatch): + monkeypatch.setattr(sched, "_direct_pr_run_still_superseded", lambda *_args: True) calls = [] head_sha = "a" * 40 stale_same_pr = { @@ -5387,6 +5586,23 @@ def fake_run(args, stdin=None): assert any("status=in_progress" in " ".join(call) for call in calls) +def test_cancel_stale_pr_runs_preserves_failed_cancellation(monkeypatch): + """Do not report a stale run cancelled when GitHub rejected the API call.""" + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr(sched, "stale_pr_run_ids", lambda _repo, _pr: ["101", "202"]) + monkeypatch.setattr(sched, "_direct_pr_run_still_superseded", lambda *_args: True) + + def cancel(_repo, run_ids): + run_id = str(run_ids[0]) + return {run_id: "GitHub rejected cancellation"} if run_id == "101" else {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + + run_ids = sched.cancel_stale_pr_runs("owner/repo", make_pr(), dry_run=False) + + assert run_ids == ["202"] + + def test_mutations_refuse_local_credentials(monkeypatch): calls = [] monkeypatch.setattr(sched, "run", lambda args: calls.append(args) or "") @@ -6906,6 +7122,7 @@ def test_draft_pr_review_only_dispatch_retries_a_failed_required_check_with_no_v def test_stale_opencode_run_ids_filters_current_head_and_missing_ids(monkeypatch): + monkeypatch.setattr(sched, "validate_git_sha", lambda value: str(value)) runs = [ {"name": "Other", "id": 10, "head_sha": "old", "pull_requests": [{"number": 1}]}, {"name": "OpenCode Review", "id": 11, "head_sha": "head", "pull_requests": [{"number": 1}]}, @@ -6920,6 +7137,7 @@ def test_stale_opencode_run_ids_filters_current_head_and_missing_ids(monkeypatch def test_workflow_run_filters_skip_mismatched_workflow_and_current_head_other_pr(monkeypatch): + monkeypatch.setattr(sched, "validate_git_sha", lambda value: str(value)) runs = [ {"name": "Other", "id": 20, "head_sha": "old", "pull_requests": [{"number": 1}]}, {"name": "OpenCode Review", "id": 21, "head_sha": "head", "pull_requests": [{"number": 2}]}, @@ -8820,3 +9038,512 @@ def test_inspect_pr_dry_run_skips_merge_revalidation_refetch(monkeypatch): assert direct_decision.action == "merge" assert auto_decision.action == "auto_merge" assert fetch_calls == [] + + + +def test_pr1669_malformed_snapshot_head_never_classifies_direct_run_stale(monkeypatch): + """Malformed snapshot head authority cannot classify a valid active run stale.""" + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda *_args, **_kwargs: [ + {"id": 33581213829, "head_sha": "a" * 40, "pull_requests": [{"number": 1528}]} + ], + ) + assert sched.stale_pr_run_ids( + "ContextualWisdomLab/naruon", + make_pr(number=1528, headRefOid="malformed-but-truthy"), + ) == [] + + +def test_pr1669_malformed_snapshot_head_never_classifies_review_run_stale(monkeypatch): + """Malformed snapshot head authority cannot classify central review runs stale.""" + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda *_args, **_kwargs: [ + { + "id": 33581213829, + "event": "pull_request", + "name": "OpenCode Review", + "head_sha": "a" * 40, + "pull_requests": [{"number": 1528}], + } + ], + ) + assert sched.active_review_run_refs( + "ContextualWisdomLab/naruon", + "OpenCode Review", + make_pr(number=1528, headRefOid="malformed-but-truthy"), + run_title="Required OpenCode Review", + workflow_aliases=frozenset(sched.OPENCODE_WORKFLOW_NAMES), + ) == ([], []) + + +def test_pr1669_snapshot_race_preserves_new_current_head(monkeypatch): + """A push after classification cannot make the new current-head run cancellable.""" + old_head, new_head = "a" * 40, "b" * 40 + candidate = { + "id": 77, + "event": "pull_request", + "status": "queued", + "head_sha": new_head, + "pull_requests": [{"number": 7}], + } + monkeypatch.setattr(sched, "stale_pr_run_ids", lambda *_args, **_kwargs: ["77"]) + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + calls = [] + + def fake_api(path): + calls.append(path) + if path.endswith("/actions/runs/77"): + return candidate + return {"state": "open", "draft": False, "head": {"sha": new_head}} + + cancelled = [] + monkeypatch.setattr(sched, "gh_api_json", fake_api) + monkeypatch.setattr( + sched, + "force_cancel_workflow_runs", + lambda *_args: cancelled.append(_args), + ) + assert sched.cancel_stale_pr_runs( + "owner/repo", make_pr(number=7, headRefOid=old_head), dry_run=False + ) == [] + assert cancelled == [] + assert calls[-1] == "repos/owner/repo/pulls/7" + + +@pytest.mark.parametrize( + "live_pr", + [ + None, + {"state": "closed", "draft": False, "head": {"sha": "b" * 40}}, + {"state": "open", "draft": None, "head": {"sha": "b" * 40}}, + {"state": "open", "draft": False, "head": {"sha": "bad"}}, + ], +) +def test_pr1669_fresh_open_pr_fails_closed_without_open_exact_head(monkeypatch, live_pr): + """Only an open PR with explicit draft state and valid SHA grants stale-run cancellation authority.""" + monkeypatch.setattr(sched, "gh_api_json", lambda _path: live_pr) + with pytest.raises(ValueError): + sched._fresh_open_pr_for_cancellation("owner/repo", 7) + + +@pytest.mark.parametrize("payload", [None, {"status": "completed"}]) +def test_pr1669_fresh_active_run_requires_active_mapping(monkeypatch, payload): + """Only a freshly active run mapping can authorize destructive cancellation.""" + monkeypatch.setattr(sched, "gh_api_json", lambda _path: payload) + with pytest.raises(ValueError, match="is not active"): + sched._fresh_active_run_for_cancellation("owner/repo", "94") + + +@pytest.mark.parametrize( + "run", + [ + { + "event": "repository_dispatch", + "status": "queued", + "head_sha": "a" * 40, + "pull_requests": [{"number": 7}], + }, + { + "event": "pull_request", + "status": "queued", + "head_sha": "a" * 40, + "pull_requests": [{"number": 8}], + }, + ], +) +def test_pr1669_direct_revalidation_rejects_changed_run_identity(monkeypatch, run): + """A direct candidate must remain a direct run attached to the target PR.""" + monkeypatch.setattr( + sched, + "gh_api_json", + lambda path: run + if "/actions/runs/" in path + else {"state": "open", "draft": False, "head": {"sha": "b" * 40}}, + ) + assert sched._direct_pr_run_still_superseded("owner/repo", 7, "93") is False + + +def test_pr1669_direct_revalidation_allows_genuine_supersession(monkeypatch): + """A genuinely older direct PR run remains cancellable after fresh reads.""" + monkeypatch.setattr( + sched, + "gh_api_json", + lambda path: { + "event": "pull_request", + "status": "in_progress", + "head_sha": "a" * 40, + "pull_requests": [{"number": 7}], + } + if "/actions/runs/" in path + else {"state": "open", "draft": False, "head": {"sha": "b" * 40}}, + ) + assert sched._direct_pr_run_still_superseded("owner/repo", 7, "98") is True + + +def test_pr1669_review_target_rejects_untrusted_dispatch_title(): + """A central dispatch without exact target identity has no cancellation authority.""" + with pytest.raises(ValueError, match="trusted target identity"): + sched._review_run_target_head( + {"event": "repository_dispatch", "display_title": "unrelated"}, + "owner/repo", + "OpenCode Review", + 7, + ) + + +def test_pr1669_review_target_rejects_changed_direct_pr_association(): + """A direct review run must remain attached to the target pull request.""" + with pytest.raises(ValueError, match="target pull request"): + sched._review_run_target_head( + { + "event": "pull_request", + "head_sha": "a" * 40, + "pull_requests": [{"number": 8}], + }, + "owner/repo", + "OpenCode Review", + 7, + ) + + +def test_pr1669_review_target_accepts_direct_and_trusted_dispatch_identity(): + """Direct and trusted central review identities expose validated target heads.""" + assert sched._review_run_target_head( + { + "event": "pull_request", + "head_sha": "a" * 40, + "pull_requests": [{"number": 7}], + }, + "owner/repo", + "OpenCode Review", + 7, + ) == "a" * 40 + assert sched._review_run_target_head( + { + "event": "repository_dispatch", + "display_title": f"Required OpenCode Review owner/repo#7@{'a' * 40}", + }, + "owner/repo", + "OpenCode Review", + 7, + ) == "a" * 40 + + +def test_pr1669_review_revalidation_handles_stale_and_current_heads(monkeypatch): + """Fresh review authority distinguishes genuine supersession from the current head.""" + run = { + "event": "repository_dispatch", + "status": "in_progress", + "display_title": f"Required OpenCode Review owner/repo#7@{'a' * 40}", + } + live_head = {"value": "b" * 40} + + def fake_api(path): + if "/actions/runs/" in path: + return run + return {"state": "open", "draft": False, "head": {"sha": live_head["value"]}} + + monkeypatch.setattr(sched, "gh_api_json", fake_api) + assert sched._review_run_still_superseded( + "owner/repo", "OpenCode Review", 7, "ContextualWisdomLab/.github", "95" + ) is True + live_head["value"] = "a" * 40 + assert sched._review_run_still_superseded( + "owner/repo", "OpenCode Review", 7, "ContextualWisdomLab/.github", "95" + ) is False + + +def test_pr1669_single_direct_candidate_cancels_only_when_revalidated_stale(monkeypatch): + """The direct single-candidate path preserves current and cancels proven stale runs.""" + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr(sched, "stale_pr_run_ids", lambda *_args, **_kwargs: ["97"]) + stale = {"value": False} + monkeypatch.setattr(sched, "_direct_pr_run_still_superseded", lambda *_args: stale["value"]) + cancelled = [] + + def cancel(repo, run_ids): + cancelled.append((repo, run_ids)) + return {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + pr = make_pr(number=7) + assert sched.cancel_stale_pr_runs("owner/repo", pr, dry_run=False) == [] + stale["value"] = True + assert sched.cancel_stale_pr_runs("owner/repo", pr, dry_run=False) == ["97"] + assert cancelled == [("owner/repo", ["97"])] + + +def test_pr1669_single_review_candidate_cancels_only_when_revalidated_stale(monkeypatch): + """The review single-candidate path preserves current and cancels proven stale runs.""" + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr( + sched, + "active_opencode_run_refs", + lambda *_args, **_kwargs: ([], [("ContextualWisdomLab/.github", "96")]), + ) + stale = {"value": False} + monkeypatch.setattr(sched, "_review_run_still_superseded", lambda *_args: stale["value"]) + cancelled = [] + + def cancel(repo, run_ids): + cancelled.append((repo, run_ids)) + return {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + pr = make_pr(number=7) + assert sched.cancel_stale_opencode_runs( + "owner/repo", "OpenCode Review", pr, dry_run=False + ) == [] + stale["value"] = True + assert sched.cancel_stale_opencode_runs( + "owner/repo", "OpenCode Review", pr, dry_run=False + ) == ["96"] + assert cancelled == [("ContextualWisdomLab/.github", ["96"])] + + + +def test_pr1669_opencode_dispatch_preserves_candidate_that_is_current_after_revalidation(monkeypatch): + """OpenCode dispatch must preserve a candidate that became the live current-head run.""" + pr = make_pr(number=7, headRefOid="b" * 40) + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr( + sched, + "active_opencode_run_refs", + lambda *_args, **_kwargs: ([], [("ContextualWisdomLab/.github", "96")]), + ) + monkeypatch.setattr( + sched, + "_review_run_still_superseded", + lambda *_args: False, + raising=False, + ) + direct_cancellations = [] + batch_cancellations = [] + dispatches = [] + monkeypatch.setattr( + sched, + "force_cancel_workflow_runs", + lambda repo, run_ids: direct_cancellations.append((repo, list(run_ids))), + ) + monkeypatch.setattr( + sched, + "force_cancel_workflow_run_refs", + lambda refs: batch_cancellations.append(list(refs)), + raising=False, + ) + monkeypatch.setattr( + sched, + "validated_pr_dispatch_fields", + lambda _pr: ("main", "c" * 40, "b" * 40), + ) + monkeypatch.setattr(sched, "validate_git_ref", lambda value: value) + monkeypatch.setattr(sched, "repository_dispatch_target", lambda _repo: "ContextualWisdomLab/.github") + monkeypatch.setattr(sched, "complete_paginated_pr_contexts", lambda *_args: []) + monkeypatch.setattr(sched, "matching_actions_run_id", lambda *_args: None) + monkeypatch.setattr(sched, "discover_opencode_required_run_id", lambda *_args: None) + monkeypatch.setattr(sched, "run_github_dispatch", lambda *args, **kwargs: dispatches.append((args, kwargs))) + + assert sched.dispatch_opencode_review("owner/repo", "OpenCode Review", pr, dry_run=False) == "already_running" + assert direct_cancellations == [] + assert batch_cancellations == [] + assert dispatches == [] + + +def test_pr1669_strix_dispatch_preserves_candidate_that_is_current_after_revalidation(monkeypatch): + """Strix dispatch must preserve a candidate that became the live current-head run.""" + pr = make_pr(number=7, headRefOid="b" * 40) + monkeypatch.setattr(sched, "matching_actions_job_id", lambda *_args: None) + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr( + sched, + "active_review_run_refs", + lambda *_args, **_kwargs: ([], [("ContextualWisdomLab/.github", "97")]), + ) + monkeypatch.setattr( + sched, + "_review_run_still_superseded", + lambda *_args: False, + raising=False, + ) + direct_cancellations = [] + batch_cancellations = [] + dispatches = [] + monkeypatch.setattr( + sched, + "force_cancel_workflow_runs", + lambda repo, run_ids: direct_cancellations.append((repo, list(run_ids))), + ) + monkeypatch.setattr( + sched, + "force_cancel_workflow_run_refs", + lambda refs: batch_cancellations.append(list(refs)), + raising=False, + ) + monkeypatch.setattr(sched, "active_workflow_runs", lambda *_args, **_kwargs: []) + monkeypatch.setattr(sched, "repository_dispatch_target", lambda _repo: "ContextualWisdomLab/.github") + monkeypatch.setattr( + sched, + "validated_pr_dispatch_fields", + lambda _pr: ("main", "c" * 40, "b" * 40), + ) + monkeypatch.setattr(sched, "run_github_dispatch", lambda *args, **kwargs: dispatches.append((args, kwargs))) + + assert sched.dispatch_strix_evidence("owner/repo", "Strix Security Scan", pr, dry_run=False) == "already_running" + assert direct_cancellations == [] + assert batch_cancellations == [] + assert dispatches == [] + + + +def test_pr1669_direct_revalidation_fails_closed_when_live_authority_is_unreadable(monkeypatch, capsys): + """Direct cancellation must preserve the candidate when fresh authority cannot be read.""" + def fail_api(_path): + raise RuntimeError("simulated live-authority outage") + + monkeypatch.setattr(sched, "gh_api_json", fail_api) + assert sched._direct_pr_run_still_superseded("owner/repo", 7, "94") is False + assert "Preserving workflow run 94 in owner/repo" in capsys.readouterr().out + + +def test_pr1669_review_revalidation_fails_closed_when_live_authority_is_unreadable(monkeypatch, capsys): + """Review cancellation must preserve the candidate when fresh authority cannot be read.""" + def fail_api(_path): + raise RuntimeError("simulated live-authority outage") + + monkeypatch.setattr(sched, "gh_api_json", fail_api) + assert sched._review_run_still_superseded( + "owner/repo", "OpenCode Review", 7, "ContextualWisdomLab/.github", "95" + ) is False + assert "Preserving review run ContextualWisdomLab/.github#95" in capsys.readouterr().out + + +def test_pr1669_revalidated_review_refs_cover_empty_and_parallel_mixed_candidates(monkeypatch): + """The review helper preserves uncertain refs and cancels only concurrently proven stale refs.""" + pr = make_pr(number=7, headRefOid="b" * 40) + assert sched._cancel_revalidated_review_run_refs( + "owner/repo", "OpenCode Review", pr, [] + ) == ([], []) + + stale = {"96": True, "97": False} + monkeypatch.setattr( + sched, + "_review_run_still_superseded", + lambda _repo, _workflow, _number, _run_repo, run_id: stale[run_id], + ) + cancelled = [] + + def cancel(repo, run_ids): + cancelled.append((repo, list(run_ids))) + return {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + preserved, cancelled_refs = sched._cancel_revalidated_review_run_refs( + "owner/repo", + "OpenCode Review", + pr, + [ + ("ContextualWisdomLab/.github", "96"), + ("ContextualWisdomLab/.github", "97"), + ], + ) + assert preserved == [("ContextualWisdomLab/.github", "97")] + assert cancelled_refs == [("ContextualWisdomLab/.github", "96")] + assert cancelled == [("ContextualWisdomLab/.github", ["96"])] + + +def test_pr1669_parallel_direct_candidates_preserve_live_and_cancel_only_stale(monkeypatch): + """Parallel direct-run cleanup must keep a revalidated current-head candidate.""" + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr(sched, "stale_pr_run_ids", lambda *_args, **_kwargs: ["94", "95"]) + monkeypatch.setattr( + sched, + "_direct_pr_run_still_superseded", + lambda _repo, _number, run_id: run_id == "94", + ) + cancelled = [] + + def cancel(repo, run_ids): + cancelled.append((repo, list(run_ids))) + return {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + assert sched.cancel_stale_pr_runs("owner/repo", make_pr(number=7), dry_run=False) == ["94"] + assert cancelled == [("owner/repo", ["94"])] + + +def test_pr1669_parallel_opencode_candidates_preserve_live_and_cancel_only_stale(monkeypatch): + """Parallel OpenCode cleanup must keep a revalidated current-head review candidate.""" + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr( + sched, + "active_opencode_run_refs", + lambda *_args, **_kwargs: ( + [], + [ + ("ContextualWisdomLab/.github", "96"), + ("ContextualWisdomLab/.github", "97"), + ], + ), + ) + monkeypatch.setattr( + sched, + "_review_run_still_superseded", + lambda _repo, _workflow, _number, _run_repo, run_id: run_id == "96", + ) + cancelled = [] + + def cancel(repo, run_ids): + cancelled.append((repo, list(run_ids))) + return {} + + monkeypatch.setattr(sched, "force_cancel_workflow_runs", cancel) + assert sched.cancel_stale_opencode_runs( + "owner/repo", "OpenCode Review", make_pr(number=7), dry_run=False + ) == ["96"] + assert cancelled == [("ContextualWisdomLab/.github", ["96"])] + + +def test_pr1669_opencode_open_draft_old_head_remains_cancellable(monkeypatch): + """An old OpenCode run on an open draft must not block current-head review-only dispatch.""" + old_head = "a" * 40 + live_head = "b" * 40 + run = { + "event": "repository_dispatch", + "status": "in_progress", + "display_title": f"Required OpenCode Review owner/repo#7@{old_head}", + } + + def fake_api(path): + if "/actions/runs/" in path: + return run + return {"state": "open", "draft": True, "head": {"sha": live_head}} + + monkeypatch.setattr(sched, "gh_api_json", fake_api) + assert sched._review_run_still_superseded( + "owner/repo", "OpenCode Review", 7, "ContextualWisdomLab/.github", "96" + ) is True + + +def test_pr1669_strix_open_draft_old_head_remains_cancellable(monkeypatch): + """An old Strix run on an open draft must not block current-head review-only dispatch.""" + old_head = "a" * 40 + live_head = "b" * 40 + run = { + "event": "repository_dispatch", + "status": "queued", + "display_title": f"Strix Security Scan owner/repo#7@{old_head}", + } + + def fake_api(path): + if "/actions/runs/" in path: + return run + return {"state": "open", "draft": True, "head": {"sha": live_head}} + + monkeypatch.setattr(sched, "gh_api_json", fake_api) + assert sched._review_run_still_superseded( + "owner/repo", "Strix Security Scan", 7, "ContextualWisdomLab/.github", "97" + ) is True diff --git a/tests/test_strix_openai_fallback_api_base.py b/tests/test_strix_openai_fallback_api_base.py index 7919a7468e..b5bf0cacb0 100644 --- a/tests/test_strix_openai_fallback_api_base.py +++ b/tests/test_strix_openai_fallback_api_base.py @@ -365,6 +365,27 @@ def test_manual_status_job_has_status_write_permission(self) -> None: job = workflow.split(" publish-manual-pr-evidence-status:", 1)[1] self.assertIn(" statuses: write", job.split(" steps:", 1)[0]) + def test_manual_status_job_has_a_bounded_runtime(self) -> None: + """A hung OIDC exchange or status POST must not inherit the 360-minute default.""" + + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + job = workflow.split(" publish-manual-pr-evidence-status:", 1)[1] + before_steps = job.split(" steps:", 1)[0] + match = re.search(r"^ timeout-minutes: (\d+)$", before_steps, flags=re.MULTILINE) + self.assertIsNotNone(match, "publish-manual-pr-evidence-status must declare a job-level timeout-minutes") + timeout = int(match.group(1)) + self.assertTrue(1 <= timeout <= 15) + + def test_cancel_superseded_pr_runs_job_has_a_bounded_runtime(self) -> None: + """A hung gh-api call in the cleanup loop must not occupy a runner for six hours.""" + + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + job = workflow.split(" cancel-superseded-pr-runs:", 1)[1].split("\n strix:", 1)[0] + match = re.search(r"^ timeout-minutes: (\d+)$", job, flags=re.MULTILINE) + self.assertIsNotNone(match, "cancel-superseded-pr-runs must declare a job-level timeout-minutes") + timeout = int(match.group(1)) + self.assertTrue(1 <= timeout <= 20) + if __name__ == "__main__": unittest.main() From 575f5a0c71048e837b62d64de92ec9dedd56e9be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:38:18 +0900 Subject: [PATCH 6/7] fix(architecture): make quarantine-sandbox-runtime the single isolation owner --- docs/CWL-MASTER-CONTEXT.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/CWL-MASTER-CONTEXT.md b/docs/CWL-MASTER-CONTEXT.md index 558b7049a9..a6ef0622e7 100644 --- a/docs/CWL-MASTER-CONTEXT.md +++ b/docs/CWL-MASTER-CONTEXT.md @@ -17,7 +17,7 @@ Turn scattered enterprise context into **judgment-ready structure, then action** naruon is fundamentally an **email workspace** that connects scattered context → judgment → action. It is **NOT groupware / HRIS / an approval-workflow (전자결재) / ERP engine.** It **OBSERVES, SYNTHESIZES, and SURFACES** judgment-ready structure to the human — it does **NOT own or execute** org processes (approval routing, recusal, escalation, HR actions, evaluations). The whole relationship / org-hierarchy / authority / norm-group / COI model (§4, §5, §5b) exists for **CONTEXT UNDERSTANDING + SURFACING**, NOT for enforcement. Example: for an in-company couple on a direct reporting line, naruon may NOTICE the multiplex tie and, when relevant, SURFACE a judgment-support flag ("this touches your partner / a possible conflict of interest") — it does NOT auto-recuse or route the approval; the actual approval/recusal lives in the external 전자결재 system, which naruon integrates with / observes but does not replace. When drilling the model, do not drift into groupware/workflow-owning features. Judgment (and org action) stays with the human + their existing systems. ## 2. naruon = the PLATFORM (one platform, many à-la-carte plugins) -`naruon` is an email-first workspace (FastAPI backend + Next.js frontend + a thin WebSocket connector proxying IMAP/SMTP/CalDAV/WebDAV from customer premises) whose core is a **dense two-tier knowledge graph** over Postgres + pgvector. It is a **TRUE plugin platform** ("진정한 plugin처럼 계속 붙일 수 있는"): plugin manifest/contract, extension points (ingest sources, DOM/analysis processors, KG enrichers, work-item types, UI panels, agents, scheduling), plugin registry, versioned API, isolated execution for untrusted plugins (noema quarantine sandbox). **À-la-carte / opt-in**: each capability is a plugin a user enables by need; nothing mandatory; different users run different combos. Every imported component is **standalone AND submodule** ("따로, 또 같이"). +`naruon` is an email-first workspace (FastAPI backend + Next.js frontend + a thin WebSocket connector proxying IMAP/SMTP/CalDAV/WebDAV from customer premises) whose core is a **dense two-tier knowledge graph** over Postgres + pgvector. It is a **TRUE plugin platform** ("진정한 plugin처럼 계속 붙일 수 있는"): plugin manifest/contract, extension points (ingest sources, DOM/analysis processors, KG enrichers, work-item types, UI panels, agents, scheduling), plugin registry, versioned API, isolated execution for untrusted plugins through **quarantine-sandbox-runtime** (see §6). **À-la-carte / opt-in**: each capability is a plugin a user enables by need; nothing mandatory; different users run different combos. Every imported component is **standalone AND submodule** ("따로, 또 같이"). ## 3. Ecosystem components (product names + roles) Product renames (repo slug → product name; domains purchased): `cwl-idp`→**keyverse** (keyverse.io), `waf-ids-ai-soc`→**wardnet** (wardnet.io), `cwl-editor`→**inkspan** (inkspan.io). Other domains: cloud-erd.app (pg-erd-cloud), naruon.net / naruon.io (naruon). @@ -33,7 +33,7 @@ Product renames (repo slug → product name; domains purchased): `cwl-idp`→**k - **codec-carver** — STT / omni-modal speech+video codec (audio/video conversion for LLM input); speaker diarization + consented voiceprint; feeds auto meeting minutes. - **fast-mlsirm** — LLM-as-a-Judge output **calibration** + measurement/evaluation-item quality; incorporate `aFIPC` Fixed-Item Parameter Calibration + `kaefa`-style item-fit optimal-model search (R IRT/psychometrics). GPU = GPGPU in the Rust core (wgpu, single numpy|rust backend axis). - **semantic-data-portal (SDP)** — the higher **ontology / catalog / governance plane** ABOVE the doc KG (Apache AGE + pgvector). naruon owns the doc KG (content_graph + project_graph in Postgres); SDP is not that store. -- **noema** — agent runtime (Pydantic-AI / Codex-Python): a GitHub Review Agent in CI + a do-anything agent inside naruon. Untrusted artifact/code analysis runs in a separately quarantined execution plane, owned by **quarantine-sandbox-runtime** (see §6), not inside the Noema Worker process (see §6's note, 2026-09-02). +- **noema** — GitHub Actions OIDC short-lived repository capability and exact-revision evidence / credential-maintenance control plane. It does **not** own untrusted artifact/code execution; that responsibility belongs to **quarantine-sandbox-runtime** (see §6). - **newsdom-api** — PDF → DOM recognition sidecar (generalized beyond JP newspapers). naruon parses non-PDF formats (html/md/plaintext) into its content_graph. - **scopeweave** — issue/WBS **management** + ITSM Service Request (two-layer: requester ticket ↔ team issues). Consumes issues naruon extracts from email/conversation/ITSR. (Dev-CODE issues stay in GitHub/GitLab — integrate, don't rebuild GitHub.) - **appguardrail** — app security guardrails; collects org security/CI failures + Strix findings as issues. @@ -188,13 +188,13 @@ flowchart TB SCOPE["scopeweave — issues / WBS / ITSM"] CODEC["codec-carver — STT / audio→minutes (+voiceprint)"] BAND["bandscope — musicians' rehearsal vertical"] - NOEMA["noema — agent runtime + review evidence"] - QUAR["quarantine-sandbox-runtime — AI SOC sandbox (§6)"] + QUAR["quarantine-sandbox-runtime — isolated artifact/code analysis (§6)"] end subgraph INFRA["Infra / governance"] CF[("Cloudflare — Pages/Workers/DNS")] GH[(".github — governance + Project #1")] + NOEMA["noema — GitHub OIDC capability + exact-revision evidence"] end P1 --> WARD @@ -224,8 +224,9 @@ flowchart TB NAR --> ERD NAR -->|"extracted issues → manage"| SCOPE CODEC -->|"diarize + minutes"| NAR - NAR --> NOEMA + GH -->|"repository capability / exact-revision evidence"| NOEMA WARD -->|"quarantine detonation"| QUAR + NAR -->|"untrusted plugin/artifact analysis"| QUAR BAND -->|"musicians also use email"| NAR BAND -. "rehearsal app" .-> P2 @@ -239,4 +240,4 @@ flowchart TB class ORCH,KEY,WARD plane; ``` -**Reading it:** users hit `wardnet` (edge/SOC) → `naruon` (platform); everything authenticates via `keyverse` (which federates external ADFS/LDAP). `naruon` ingests via the `connector` + `newsdom-api`, builds the KG in Postgres, uses `semantic-data-portal` for the ontology plane, and routes ALL LLM work through `contextual-orchestrator` (which load-balances upstreams and routes batch to `pg-llm-batch`). `noema` is the shared agent runtime + quarantine sandbox (used by naruon, the GitHub review agent, and wardnet's AI SOC). Plugins/verticals (`inkspan`, `clearfolio`, `pg-erd-cloud`, `scopeweave`, `codec-carver`, `bandscope`) attach à-la-carte; `fast-mlsirm` calibrates LLM-as-Judge quality. Hosting = Cloudflare; governance + Project #1 live in `.github`. +**Reading it:** users hit `wardnet` (edge/SOC) → `naruon` (platform); everything authenticates via `keyverse` (which federates external ADFS/LDAP). `naruon` ingests via the `connector` + `newsdom-api`, builds the KG in Postgres, uses `semantic-data-portal` for the ontology plane, and routes ALL LLM work through `contextual-orchestrator` (which load-balances upstreams and routes batch to `pg-llm-batch`). `noema` is the GitHub OIDC/exact-revision evidence control plane and uses `contextual-orchestrator` for model-backed work; `quarantine-sandbox-runtime` owns isolated untrusted artifact/code analysis for wardnet and product/plugin consumers. Plugins/verticals (`inkspan`, `clearfolio`, `pg-erd-cloud`, `scopeweave`, `codec-carver`, `bandscope`) attach à-la-carte; `fast-mlsirm` calibrates LLM-as-Judge quality. Hosting = Cloudflare; governance + Project #1 live in `.github`. From 7deae853bbb7697dab79c235b21340ba03f5f7ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:39:41 +0900 Subject: [PATCH 7/7] test(architecture): reject competing Noema quarantine ownership --- tests/test_product_technical_gap_baseline.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_product_technical_gap_baseline.py b/tests/test_product_technical_gap_baseline.py index d44ffdb8e6..9a9f7f4dbe 100644 --- a/tests/test_product_technical_gap_baseline.py +++ b/tests/test_product_technical_gap_baseline.py @@ -98,3 +98,14 @@ def test_master_context_points_at_live_baseline_without_freezing_shas() -> None: assert "ContextualWisdomLab/naruon#975" in source assert "Done" in source assert "merge authorization" in source + + +def test_master_context_has_one_quarantine_isolation_owner() -> None: + """Noema must not compete with quarantine-sandbox-runtime for isolation ownership.""" + source = Path("docs/CWL-MASTER-CONTEXT.md").read_text(encoding="utf-8") + assert "isolated execution for untrusted plugins (noema quarantine sandbox)" not in source + assert "`noema` is the shared agent runtime + quarantine sandbox" not in source + assert "**noema** — GitHub Actions OIDC short-lived repository capability" in source + assert "**quarantine-sandbox-runtime**" in source + assert "WARD -->|\"quarantine detonation\"| QUAR" in source + assert "GH -->|\"repository capability / exact-revision evidence\"| NOEMA" in source