diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index 16c53522e..e32e69e2c 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -12,6 +12,7 @@ on: - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml - .github/workflows/governance-risk-compliance-hourly-review-repair.yml + - .github/workflows/inkspan-hourly-review-repair.yml - .github/workflows/hourly-nvidia-nim-review-repair.yml - .github/workflows/nonnest2-hourly-review-repair.yml - .github/workflows/orgmetra-hourly-review-repair.yml @@ -25,6 +26,7 @@ on: - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py - tests/test_governance_risk_compliance_hourly_review_caller.py + - tests/test_inkspan_hourly_review_caller.py - tests/test_hourly_scheduler_runtime_budget.py - tests/test_nonnest2_hourly_review_caller.py - tests/test_orgmetra_hourly_review_caller.py @@ -51,6 +53,7 @@ on: - docs/doctoring/fast-mlsirm-hourly-review-caller.md - docs/doctoring/github-hourly-conflict-repair.md - docs/doctoring/governance-risk-compliance-hourly-review-caller.md + - docs/doctoring/inkspan-hourly-review-caller.md - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/orgmetra-hourly-review-caller.md @@ -68,6 +71,7 @@ on: - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml - .github/workflows/governance-risk-compliance-hourly-review-repair.yml + - .github/workflows/inkspan-hourly-review-repair.yml - .github/workflows/hourly-nvidia-nim-review-repair.yml - .github/workflows/nonnest2-hourly-review-repair.yml - .github/workflows/orgmetra-hourly-review-repair.yml @@ -81,6 +85,7 @@ on: - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py - tests/test_governance_risk_compliance_hourly_review_caller.py + - tests/test_inkspan_hourly_review_caller.py - tests/test_hourly_scheduler_runtime_budget.py - tests/test_nonnest2_hourly_review_caller.py - tests/test_orgmetra_hourly_review_caller.py @@ -107,6 +112,7 @@ on: - docs/doctoring/fast-mlsirm-hourly-review-caller.md - docs/doctoring/github-hourly-conflict-repair.md - docs/doctoring/governance-risk-compliance-hourly-review-caller.md + - docs/doctoring/inkspan-hourly-review-caller.md - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/orgmetra-hourly-review-caller.md @@ -165,6 +171,7 @@ jobs: tests/test_fast_mlsirm_hourly_review_caller.py \ tests/test_github_hourly_conflict_repair.py \ tests/test_governance_risk_compliance_hourly_review_caller.py \ + tests/test_inkspan_hourly_review_caller.py \ tests/test_hourly_scheduler_runtime_budget.py \ tests/test_nonnest2_hourly_review_caller.py \ tests/test_orgmetra_hourly_review_caller.py \ diff --git a/.github/workflows/inkspan-hourly-review-repair.yml b/.github/workflows/inkspan-hourly-review-repair.yml new file mode 100644 index 000000000..daa1ce630 --- /dev/null +++ b/.github/workflows/inkspan-hourly-review-repair.yml @@ -0,0 +1,30 @@ +name: Inkspan Hourly Review Repair + +on: + schedule: + # Minute 47 avoids the other product-specific review-repair heartbeats. + - cron: "47 * * * *" + +concurrency: + group: inkspan-hourly-review-repair + # A later heartbeat must not cancel an in-flight editor-safety RCA. + cancel-in-progress: false + +permissions: + contents: read + +jobs: + dispatch-review-repair: + permissions: + contents: read + id-token: write + uses: ./.github/workflows/pr-review-fix-scheduler.yml + with: + target_repository: ContextualWisdomLab/inkspan + base_branch: main + max_prs: "50" + max_dispatches: "1" + retry_hours: "2" + secrets: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0ef8d44..c70c85b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Semantic Versioning where the repository publishes a release. - Added a dedicated Clearfolio hourly caller that invokes the product-neutral central scheduler with the exact repository, protected base branch, one-dispatch budget, one-hour retry floor, single-flight concurrency, and only the established scheduler credentials. - Added a dedicated DiskSage hourly caller that invokes the same product-neutral RCA and remediation-feasibility scheduler with an exact repository target, one-dispatch budget, two-hour same-head retry floor, non-cancelling single-flight heartbeat, and explicit established scheduler credentials. - Added a dedicated fast-mlsirm hourly caller that preserves Rust-owned psychometric arithmetic while dispatching at most one exact-head, root-cause-driven repair with a two-hour same-head retry floor. +- Added a dedicated Inkspan hourly caller that invokes the reusable root-cause and remediation-feasibility scheduler for the exact protected-main editor queue at minute 47, with one-dispatch scope, a two-hour same-head retry floor, non-cancelling concurrency, job-scoped OIDC, and no caller-side model credential. - Added a dedicated Orgmetra hourly caller at minute 58 that targets protected `develop`, dispatches at most one exact-head repair, preserves a two-hour same-head retry floor and non-cancelling single-flight execution, and maps only the established scheduler credentials. ### Changed diff --git a/docs/automation/hourly-review-repair.md b/docs/automation/hourly-review-repair.md index 722724958..a927596e0 100644 --- a/docs/automation/hourly-review-repair.md +++ b/docs/automation/hourly-review-repair.md @@ -5,6 +5,8 @@ engine**. - `clearfolio-hourly-review-repair.yml` owns Clearfolio's heartbeat at minute 23 of every hour. +- `inkspan-hourly-review-repair.yml` owns Inkspan's heartbeat at minute 47 with + a two-hour same-head retry floor. - `orgmetra-hourly-review-repair.yml` owns Orgmetra's heartbeat at minute 58 of every hour against protected `develop`. - `pr-review-fix-scheduler.yml` is the reusable, product-neutral scheduler @@ -47,6 +49,25 @@ The caller passes only the established `PR_REVIEW_MERGE_TOKEN` and `NVIDIA_NIM_API_KEY`; the model credential is scoped exclusively to the two OpenCode execution steps in the separately reviewed autofix worker. +## Inkspan execution contract + +The Inkspan caller is a thin consumer of the same reusable scheduler: + +```yaml +target_repository: ContextualWisdomLab/inkspan +base_branch: main +max_prs: "50" +max_dispatches: "1" +retry_hours: "2" +``` + +It runs at `47 * * * *`, uses its own non-cancelling single-flight group, and +passes only the two established scheduler credentials. The caller grants the +reusable job `id-token: write` for the existing OIDC App-token fallback but +does not receive model credentials. Before protected-main activation, +`OPENCODE_REPOSITORY_DISPATCH_TARGETS` must include the exact Inkspan +repository; a missing mapping fails before mutation credential materialization. + ## Orgmetra execution contract The Orgmetra caller provides the following immutable operating parameters: @@ -221,8 +242,11 @@ Permanent tests prove: - the Clearfolio caller owns exactly one hourly schedule and names the exact repository and protected base branch; +- the Inkspan caller owns its minute-47 schedule, exact repository and base, + non-cancelling concurrency, OIDC boundary, and explicit secret contract; - the shared scheduler contains no product-specific timer or repository name; -- the dispatch budget and same-head retry floor remain one; +- each caller retains its declared one-dispatch budget and same-head retry + floor; - caller and reusable-workflow secrets are explicit and never use `secrets: inherit`; - immutable source, NVIDIA-only model authentication, child-process credential diff --git a/docs/doctoring/inkspan-hourly-review-caller.md b/docs/doctoring/inkspan-hourly-review-caller.md new file mode 100644 index 000000000..34421b678 --- /dev/null +++ b/docs/doctoring/inkspan-hourly-review-caller.md @@ -0,0 +1,140 @@ +# Inkspan hourly review-repair caller + +검토 기준일: **2026-08-24** + +## Decision + +ContextualWisdomLab operates one protected hourly caller for +`ContextualWisdomLab/inkspan`, the standalone TipTap/ProseMirror Markdown and +HTML editor module with bounded Office import, base64 image conversion, +accessibility, and host-integration contracts. The caller runs at minute 47, +delegates to the product-neutral central review-fix scheduler, inspects at most +50 open pull requests targeting protected `main`, and dispatches at most one +bounded repair per heartbeat. + +The repository had 52 open `main` pull requests at this decision snapshot. +Buyer-visible work included ContextualWisdomLab/inkspan#373 (dependency +security), ContextualWisdomLab/inkspan#372 (product-gap evidence), +ContextualWisdomLab/inkspan#362 (editor accessibility), and +ContextualWisdomLab/inkspan#299 (stacked exact-head gates). Leaving this queue +outside the dedicated hourly repair frontier makes reviewed editor safety, +accessibility, and release fixes wait behind unrelated products. + +The caller does not implement product, review, or mutation logic. Inkspan keeps +ownership of editor state, document formats, host APIs, design tokens, +Storybook inventory, persistence, and release semantics. Privileged automation +stays in `ContextualWisdomLab/.github`, and consumers can continue importing +Inkspan as a module without the central repository at runtime. + +## Root-cause analysis and remediation feasibility + +The reusable worker performs exact-head root-cause analysis and tests +remediation feasibility before it edits. The reusable worker must: + +1. Refetch the exact live head, base, reviews, checks, changed paths, stack + dependencies, and writer state. +2. Establish the causal chain rather than repeat a terminal symptom. +3. Enumerate materially distinct minimal remedies and prefer existing project, + platform, or dependency behavior before adding code. +4. Reject remedies that lack writer authority, cross sealed paths, require + unavailable credentials or protected-setting changes, violate stack order, + cannot be verified, or do not alter the diagnosed cause. +5. Dispatch at most one feasible repair; otherwise leave the tree unchanged and + continue with another eligible head. + +A queued or pending check remains a merge blocker but is not itself a source +finding. The independent non-author approval remains an external authorization +gate and is never synthesized by the repair worker. The worker cannot approve, +merge, release, resolve review findings by inference, change protection, or +manufacture passing checks. + +## Cadence and concurrency + +The caller uses its own concurrency group and `cancel-in-progress: false`. A +new heartbeat therefore cannot discard an editor-safety RCA or exact-head test +run already in progress. The reusable scheduler may cancel only its own +superseded short queue scan. + +The caller sets a **two-hour same-head retry floor**. Central OpenCode, Strix, +Noema, and NVIDIA NIM work can legitimately exceed one hour, and the user has +explicitly prioritized accuracy over latency. An hourly heartbeat still finds +new heads immediately, while an unchanged head cannot create duplicate writer +pressure before the active evidence window has elapsed. + +GitHub scheduled workflows execute from the default branch and may be delayed +under load. The cron expression is a durable heartbeat rather than a real-time +service-level promise (GitHub, n.d.-a). + +## Credential and model boundary + +The caller keeps workflow `GITHUB_TOKEN` at `contents: read` and grants the +reusable job `id-token: write` so the central scheduler can exchange GitHub OIDC +for its OpenCode GitHub App token when a mapped PAT is unavailable (GitHub, +n.d.-c). It maps only `PR_REVIEW_MERGE_TOKEN` and +`OPENCODE_APPROVE_TOKEN`, never uses `secrets: inherit`, never receives +`NVIDIA_NIM_API_KEY`, and never introduces `COPILOT_GITHUB_TOKEN`. + +Model discovery, provider selection, reasoning effort, and LLM execution remain +inside the separately reviewed central worker and contextual-orchestrator +boundary. The thin caller contains no model name, provider API, temperature, +fallback list, or prompt. CWE-250 forbids giving this read-only scheduler +surface write or model privileges it does not need (MITRE, 2026). + +Before protected-main activation, the repository variable +`OPENCODE_REPOSITORY_DISPATCH_TARGETS` must contain the exact +`ContextualWisdomLab/inkspan` target. A missing or mismatched value fails before +mutation credential materialization. GitHub App installation and mapped +credentials must also remain limited to approved repositories. + +## Security, standalone operation, and modularity + +The caller adds no Inkspan runtime dependency, database object, network +endpoint, tenant authority, customer document, or product credential. It never +executes pull-request code with mutation secrets. Inkspan remains independently +buildable, testable, releasable, and consumable by LineageWeave, naruon, or +other hosts; the central repository only coordinates repository maintenance. + +The control follows SSDF's separation of protected build/automation controls +from untrusted contribution content and records a machine-verifiable least- +privilege boundary rather than relying on operator memory (NIST, 2022). + +## Verification and rollback + +Machine-checkable contracts require the exact repository and `main` base, +minute-47 cadence, non-cancelling single-flight group, one-dispatch budget, +two-hour retry floor, explicit secret mapping, read-only contents plus +job-scoped `id-token: write`, focused path-filter coverage, and absence of model +or Copilot credentials. Independent `pull_request`, `push`, and `compileall` +blocks each name the caller, doctoring, or contract they own. + +After source integration, closure requires a scheduled protected-main consumer +run proving the exact Inkspan target and `main` base, read-only caller token, +OIDC/PAT fail-closed behavior, and bounded dispatch decision. Source checks +alone are not protected-main operational acceptance. Merge still requires +terminal protected checks, zero unresolved valid findings, and a qualifying +independent non-author approval. + +Rollback removes the Inkspan caller, its focused test, doctoring, and central +path-filter/documentation entries. It must not remove scheduler validation or +affect any independent product caller. + +## APA 7th references + +GitHub, Inc. (n.d.-a). *Events that trigger workflows*. GitHub Docs. Retrieved +August 24, 2026, from +https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule + +GitHub, Inc. (n.d.-b). *Reuse workflows*. GitHub Docs. Retrieved August 24, +2026, from +https://docs.github.com/en/actions/how-tos/sharing-automations/reuse-workflows + +GitHub, Inc. (n.d.-c). *OpenID Connect*. GitHub Docs. Retrieved August 24, +2026, from https://docs.github.com/en/actions/concepts/security/openid-connect + +MITRE. (2026). *CWE-250: Execution with unnecessary privileges*. +https://cwe.mitre.org/data/definitions/250.html + +National Institute of Standards and Technology. (2022). *Secure software +development framework (SSDF) version 1.1: Recommendations for mitigating the +risk of software vulnerabilities* (NIST Special Publication 800-218). +https://doi.org/10.6028/NIST.SP.800-218 diff --git a/tests/test_inkspan_hourly_review_caller.py b/tests/test_inkspan_hourly_review_caller.py new file mode 100644 index 000000000..eb2c05ba0 --- /dev/null +++ b/tests/test_inkspan_hourly_review_caller.py @@ -0,0 +1,164 @@ +"""Contract tests for Inkspan's bounded hourly review-repair caller.""" + +from pathlib import Path + + +CALLER = Path(".github/workflows/inkspan-hourly-review-repair.yml") +DOCTORING = Path("docs/doctoring/inkspan-hourly-review-caller.md") +QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") +SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") + + +def _read(path: Path) -> str: + """Return one repository contract file as UTF-8 text.""" + return path.read_text(encoding="utf-8") + + +def _yaml_path_entries(block: str) -> set[str]: + """Return dashed YAML path entries from one trigger or compileall block.""" + entries: set[str] = set() + for raw_line in block.splitlines(): + stripped = raw_line.strip() + if stripped.startswith("- "): + entries.add(stripped[2:].strip()) + elif stripped.startswith("tests/") or stripped.startswith("scripts/"): + entries.add(stripped.rstrip(" \\")) + return entries + + +def _trigger_path_block(quality: str, trigger: str) -> str: + """Return the dashed path list under one named workflow trigger.""" + marker = f" {trigger}:\n paths:\n" + start = quality.index(marker) + len(marker) + lines: list[str] = [] + for line in quality[start:].splitlines(): + if line.startswith(" - "): + lines.append(line) + continue + if line.strip() == "": + continue + break + return "\n".join(lines) + + +def _compileall_block(quality: str) -> str: + """Return the compileall argument list before the whitespace gate.""" + marker = "python -m compileall -q \\" + start = quality.index(marker) + remainder = quality[start:] + end = remainder.find("\n git ") + return remainder if end < 0 else remainder[:end] + + +def test_inkspan_caller_is_hourly_bounded_and_non_cancelling() -> None: + """Inkspan receives one bounded editor repair without cancellation.""" + caller = _read(CALLER) + + assert 'cron: "47 * * * *"' in caller + assert "group: inkspan-hourly-review-repair" in caller + assert "cancel-in-progress: false" in caller + assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller + assert "target_repository: ContextualWisdomLab/inkspan" in caller + assert "base_branch: main" in caller + assert 'max_prs: "50"' in caller + assert 'max_dispatches: "1"' in caller + assert 'retry_hours: "2"' in caller + + +def test_inkspan_caller_preserves_oidc_and_explicit_secret_scope() -> None: + """The queue scanner maps scheduler credentials without model secrets.""" + caller = _read(CALLER) + workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) + + assert "\npermissions:\n contents: read\n" in workflow_scope + assert ( + "\n permissions:\n contents: read\n id-token: write\n" + in jobs_scope + ) + assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller + assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller + assert "secrets: inherit" not in caller + assert "NVIDIA_NIM_API_KEY" not in caller + assert "COPILOT_GITHUB_TOKEN" not in caller + for forbidden in ( + "actions: write", + "contents: write", + "issues: write", + "pull-requests: write", + "statuses: write", + ): + assert forbidden not in caller + + +def test_inkspan_target_is_not_hard_coded_in_shared_scheduler() -> None: + """Product identity remains in the thin caller rather than the engine.""" + assert "ContextualWisdomLab/inkspan" not in _read(SCHEDULER) + + +def test_inkspan_doctoring_records_product_and_authority_boundaries() -> None: + """Operators retain product evidence, authority, and acceptance gates.""" + doctoring = _read(DOCTORING) + + for phrase in ( + "ContextualWisdomLab/inkspan", + "OPENCODE_REPOSITORY_DISPATCH_TARGETS", + "independent non-author approval", + "NVIDIA_NIM_API_KEY", + "COPILOT_GITHUB_TOKEN", + "id-token: write", + "two-hour same-head retry floor", + "root-cause analysis", + "remediation feasibility", + "protected-main operational acceptance", + "APA 7th references", + "ContextualWisdomLab/inkspan#373", + "ContextualWisdomLab/inkspan#372", + "ContextualWisdomLab/inkspan#362", + "ContextualWisdomLab/inkspan#299", + ): + assert phrase in doctoring + + +def test_path_helpers_keep_trigger_and_compileall_sets_disjoint() -> None: + """A path in only one quality block cannot satisfy the other blocks.""" + quality = ( + "on:\n" + " pull_request:\n" + " paths:\n" + " - .github/workflows/inkspan-hourly-review-repair.yml\n" + " push:\n" + " paths:\n" + " - docs/doctoring/inkspan-hourly-review-caller.md\n" + " python -m compileall -q \\\n" + " tests/test_inkspan_hourly_review_caller.py\n" + " git diff --check\n" + ) + + pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) + push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) + compileall_paths = _yaml_path_entries(_compileall_block(quality)) + + assert pull_request_paths == {".github/workflows/inkspan-hourly-review-repair.yml"} + assert push_paths == {"docs/doctoring/inkspan-hourly-review-caller.md"} + assert compileall_paths == {"tests/test_inkspan_hourly_review_caller.py"} + + +def test_focused_quality_workflow_tracks_inkspan_contracts() -> None: + """Caller, test, and doctoring edits always rerun the focused gate.""" + quality = _read(QUALITY_WORKFLOW) + pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) + push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) + compileall_paths = _yaml_path_entries(_compileall_block(quality)) + caller = ".github/workflows/inkspan-hourly-review-repair.yml" + doctoring = "docs/doctoring/inkspan-hourly-review-caller.md" + contract = "tests/test_inkspan_hourly_review_caller.py" + + assert caller in pull_request_paths + assert doctoring in pull_request_paths + assert contract in pull_request_paths + assert caller in push_paths + assert doctoring in push_paths + assert contract in push_paths + assert contract in compileall_paths + assert caller not in compileall_paths + assert doctoring not in compileall_paths