| OQ-1 |
~~Project name~~ Resolved: assent (D-009); repo live (D-014). Domain: assent.dev taken (D-028). Path: Option A — register an owned domain (D-031). Exact domain string still TBD before rename. |
Phase 4 / before public freeze marketing |
naming.md; D-028/D-031 |
| OQ-2 |
~~Hosting: GitHub only, or GitLab mirror (dogfooding the GitLab adapter on our own repo)?~~ Resolved (D-105): defer mirror — GitHub canonical; optional read-only GitLab mirror is operator infra, not E9 blocker. Dual-primary rejected (drift risk). No mirror workflow in E9. |
— |
E9-S11 |
| OQ-3 |
~~Two parallel frontends?~~ Resolved by ADR-0002 v2: one YAML envelope, pluggable predicate backends |
— |
superseded; successor questions: OQ-11/OQ-12 |
| OQ-4 |
~~Ship gRPC (go-plugin) tier in v1?~~ Resolved (P2-E5): defer gRPC to post-v1; HTTP/exec + builtins only (Spike C, D-012, ADR-0004 Accepted) |
— |
adr-acceptance-review.md |
| OQ-5 |
~~Policy discovery: remote packs in v1?~~ Resolved (P2-E5): local .assent/ only in v1; remote packs designed-for (ADR-0010 Accepted, D-012) |
— |
adr-acceptance-review.md |
| OQ-6 |
~~E2E default in CI: kind vs testcontainer?~~ Resolved (P2-E5 / Spike B): testcontainer in CI; kind stays for local/demo (spike-b-e2e.md — boot p50 96 s vs 126 s, ~2.4 GB vs ~3.1 GB+node, 0 flakes) |
— |
ADR-0006 Accepted |
| OQ-7 |
~~GitHub mapping for challenge~~ Resolved (P2-E5): parity for the gate, not the device — required-conversation-resolution carries acknowledgement; REQUEST_CHANGES reserved for block (forge-dossier-github.md §3). Residual live checks → Phase 5 / E10 |
Phase 5 / E10 |
ADR-0005 Accepted |
| OQ-8 |
Decision replay/audit: JSON report artifact enough, or signed/attested decision record later? |
Phase 3 |
v1: artifact (Pins in report); attestations later epic |
| OQ-9 |
Version pinning for reproducibility (tool digest + policy SHA in report Pins)? |
Phase 3 |
must be in the report schema from day 1 |
| OQ-10 |
Monorepo support: multiple policy scopes per repo (path-scoped .assent/ dirs)? |
Phase 3 |
likely bindings-level path scoping |
| OQ-11 |
~~kyverno-json vs cel-go~~ Resolved (P2-E5): cel-go (ADR-0013 Accepted; Spike A) |
— |
adr-acceptance-review.md |
| OQ-12 |
~~assert authored syntax~~ Resolved (P2-E5): hybrid all/any/not trees with CEL leaves + per-leaf message (ADR-0013 Accepted) |
— |
adr-acceptance-review.md |
| OQ-13 |
~~Risk score conventions / effect escalation?~~ Resolved (P2-E5): points + per-binding thresholds only in v1; no score→effect escalation (ADR-0007 Accepted) |
— |
adr-acceptance-review.md |
| OQ-14 |
~~serve (webhook) in v1?~~ Resolved (P2-E5): v1.x / E12 post-Phase-4; architecture-ready from day 1 (ADR-0009 Accepted, D-012/D-017) |
— |
adr-acceptance-review.md |
| OQ-15 |
~~fold-to-rename opt-in?~~ Resolved (P2-E5): opt-in per class, default raw; rename never laxer than delete (ADR-0003 Accepted). Residual: similarity metric itself |
Phase 3 |
adr-acceptance-review.md |
| OQ-17 |
~~max_age default~~ Partially resolved (P2-E5): Spike C host defaults (principal/authz 1h, registry 24h, sensitive 15m; arming precondition). Schema freeze |
Phase 3 / contract fixture |
spike-c-provider.md; adr-acceptance-review.md |
| OQ-23 |
~~require-review forge mechanics~~ Leading answer (P1-E3-S02): Premium/Ultimate evidence chain = approval_rules → eligible_approvers[] → approval_state.rules[].approved_by[]; exclude MR-author and bot; never trust rule-level approved alone. Free tier: capability gap → no auto-merge for archetypes needing require-review |
P3-E1 schema slice (ApprovalEvidence per D-017) |
forge-dossier-gitlab.md §4 |
| OQ-24 |
~~Secure-setup adoption spike (topology)~~ Topology resolved (P2-E5 / P2-E4): GitLab Premium + external CI config + project access token + all-threads-resolved + merged-results + merge trains + approval rules + .assent/** human residual. North-star <1h still PENDING — operator timed clean-room run remains an open Phase-4 backlog item (do not claim confirmed). See backlog.md Phase-4 operator row. |
Phase 4 / north-star wording |
spike-secure-setup.md § North-star; timed run → HOLDS/AMEND |
| OQ-25 |
~~Success metric (roast P2-8)~~ Leading answer: independently defined routine denominator + blind holdout (labeler ≠ policy author) + ≤1% false-auto-merge budget; measure via scan/stats confusion matrix — see success-metric.md. Residual: operator adjudicates holdout labels (not invented in-tree). |
Phase 4 (adjudication) |
protocol frozen in P1-E2-S03; adjudication = operator task |
| OQ-18 |
~~GitHub arm-and-wait parity~~ Resolved (P2-E5): yes on paper with three deltas (dismissal, auto-merge revoke, merge queue) — forge-dossier-github.md §1 C8′/C11/C14, §3. Implement |
Phase 5 / E8–E10 |
ADR-0005 Accepted |
| OQ-19 |
~~Post-merge reconciliation: v1.x or out of scope?~~ In scope (D-017 B8): E12 service tier, implemented post-Phase-4 — commit↔DecisionRecord/PublicationReceipt correlation, durable safety event, optional revert MR (never direct revert) |
E12 (unlocked) |
adjudicated outcomes feed policy comparison; a human revert is evidence, not proof |
| OQ-20 |
~~Batch/sweep apply mode — or per-MR CI + serve enough?~~ In scope (D-017 B9): E12 service tier — one serialized sweep, every write through per-MR preconditions/reconciliation/budgets, no bulk bypass |
E12 (unlocked) |
scan stays recorder-only; horizontal workers unsupported until a lease exists |
| OQ-21 |
~~Per-rule rollout phases — or effect-editing sufficient?~~ Reversed (D-017 B2): explicit off/observe/enforce phase field — effect-editing loses policy identity and breaks before/after comparison; observed vs enforcing findings both recorded |
P3-E4 / ADR-0018 |
observe can never alter the enforcing decision or forge state |
| OQ-22 |
Envelope match on MR metadata: labels, draft status, author allowlists — which belong in match.mr for v1? |
Phase 3 |
draft-MRs likely skipped by default in CI template |
| OQ-26 |
assent test score.total faithfulness (P5-E6-S03). The S03 matcher computes score.total as Σ finding.Points over the enforcing Result.Findings, but a finding carries the AUTHORED per-firing weight r.Points, not firings*r.Points (the engine's real pointsSum, an intentional aggregate asymmetry, ADR-0007 Amendment 2). So for a rule that fires K>1 times the matcher UNDERcounts — safe (it can only mismatch/FAIL, never spuriously pass on higher real risk) but not faithful. A faithful total needs the engine to expose the summed pointsSum on Result (a decision-path change, its OWN fail-safety-reviewed lane — parallel to the findings[].path field-add, D-054(b)). Until then S03 fixtures are single-firing so total is exact. |
E6 fast-follow / engine lane |
logged by S03; leading answer: add Result.PointsTotal in the path/score engine lane, then Match reads it |
| OQ-28 |
~~Filesystem containment for provider reads: is PATH containment enough, or must the injected FS itself be a security boundary? (raised P5-E5-S07/S08 while implementing builtin/repo-file and builtin/resource-owner.) The builtins clip candidates to declared roots with pure string guards (cleanRel/underAnyRoot) over an os.DirFS. Under --checkout that FS is the merge request's own HEAD tree — contributor-authored content — and Go documents os.DirFS as not a security boundary while fs.Stat follows links. Question: does the invariant "never a fact from outside the declared roots" need a syscall-level root, a per-component symlink refusal, or both?~~ Resolved (D-129): BOTH, and they are not substitutes. (a) cmd/assent injects builtin.OpenRepoRoot = os.OpenRoot + (*os.Root).FS(), a syscall-level boundary for every consumer of that FS; (b) classifyCandidate Lstats every path component and refuses any symlinked candidate — the only layer that can protect the roots clip, which os.Root cannot see. In-root symlinks are refused too; refusal is unavailable with a contributor-readable reason and STOPS the walk-up. Retroactive row: D-129 and REQ-E5-S07-03 cited "OQ-28" before this table carried it (AGENTS.md rule 6 — no dangling references). |
— (closed) |
decisions.md D-129/D-130; REQ-E5-S07-03/REQ-E5-S08-03. Residual CLOSED (D-133): collectTree's silent truncation (P0) and readIfPresent's governed-subject symlink (P1) are both fixed in cmd/assent/checkout.go. Proof relocated — stated here so nobody re-derives it wrongly: D-133 refuses ANY symlink under base//head/ at changed-file ENUMERATION, before providers resolve, so this row's escape is no longer reproducible end-to-end through assent run --checkout. The provider guard is now defence in depth, proven at cmd/assent's production fact-resolution seam (TestResolveRunFactsRefusesSymlinkedQuotaCandidate, which pins the two layers separately) plus internal/provider/builtin/{repo_file,resource_owner}_symlink_test.go; it becomes the live barrier again if ADR-0008 Amendment 2's fold-the-refusal-opaque direction lands — see D-129's 2026-08-09 amendment |
| OQ-16 |
~~Which open-source repos join the demo/test corpus?~~ Resolved (P2-E5): kafka/org + JulieOps descriptors + octoDNS zones, pinned by SHA with vendored excerpts — see examples/repos/corpus.md |
— |
adr-acceptance-review.md; D-008/D-029 extra private shapes deferred but kept in corpus plan |
| OQ-29 |
PolicyProfile.spec.writes: false is a frozen-schema field with NO runtime enforcement, and lint compels adopters to author it. Operator ruling needed. docs/architecture/policy-profiles.md states the recorder-only guarantee as an "architectural invariant, not a runtime best-effort check" — line 13: a writes: false profile "Never calls Reconcile — no approve, merge, block, thread sync, or other forge write". No code enforces it, because nothing on the write path reads it. Verified by grep over non-test sources: aggregate.ResolveProfile and Result.WriteAllowed have consumers only in internal/lint/posture.go and inside internal/core/aggregate itself; aggregate.CoverWithProfile is called only from internal/compare; policy.LoadProfile is called only from cmd/assent/compare.go; and cmd/assent/run.go contains the string Profile zero times — it evaluates via aggregate.CoverWithPhaseCeiling (run.go:533) and reaches buildDesired/forge.Reconcile without ever loading or consulting a profile. internal/core/aggregate/profile.go:97 documents the missing link in its own words: "A downstream forge step reads WriteAllowed to know whether this run may arm/merge or is recorder-only" — there is no such downstream forge step. So a writes: false profile does not make assent run recorder-only; the run behaves exactly as if no profile existed. Why it is not merely internal: writes is a REQUIRED field of the frozen schemas/policy/v1alpha1/profile.schema.json, whose description reads "true = this profile authorizes forge writes for bindings in its scope; false = recorder-only", and the single-writer-profile lint hard error (internal/lint/posture.go:83) fails a tree where zero or more than one writes: true profile covers a binding — so adopters are compelled to author a field whose false value does not do what the schema says. RAISED TO P1 on 2026-08-09 — the stated escalation condition was ALREADY TRUE when it was written (audit DOC-04). The original text read: "Severity today is P2 only because docs/architecture/policy-profiles.md is NOT in the mkdocs nav, so the invariant claim is not on the docs site. If that directory ever enters the nav it becomes P1." That rests on a false premise — MkDocs publishes every file in docs_dir regardless of nav; the nav controls navigation, not publication. Measured live on 2026-08-09, not reasoned: curl -sI https://platformrelay.github.io/Assent/architecture/policy-profiles/ returns 200; sitemap.xml carries 63 <loc> entries against ~10 nav entries; docs/planning/** is fully published too; and the page's own words — the recorder-only guarantee stated as an "architectural invariant, not a runtime best-effort check" — are in the site's search/search_index.json, which indexes 420 sections and returns architecture/policy-profiles/#write-vs-recorder-only for that phrase. So the published false safety guarantee is not hypothetical; it has been live the whole time, and it is searchable. This is the D-134 shape exactly, and it is P1 by this question's own criterion. GUIDELINES.md's "docs published on the future site = product docs under docs/ only; docs/planning/, openspec/, and agent-context stay out of the mkdocs nav" is read as a publication boundary; it creates only a NAV boundary, and nothing enforces the intended one — a second, separate gap worth closing (an exclude_docs/not_in_nav setting, or moving non-product pages out of docs_dir). Ruling needed, deliberately not taken here: (a) implement the gate — load the covering profile on the run path and refuse Reconcile when WriteAllowed is false, making the documented invariant real; (b) retract the invariant language, restate spec.writes as comparison-scope metadata only, and say so in the schema description; or (c) accept the gap explicitly and annotate the doc, as ADR-0009 was annotated. Not to be resolved by silently changing the frozen schema or the lint rule — writes is a frozen contract field and the lint rule is load-bearing for the compare path. |
P1 — both stated conditions are already met: the page is published (200) and indexed, and v0.1.0 already shipped the recorder-only guarantee. Needs a ruling before v0.2.1 |
Found during the D-134/D-135 docs-truth lane (review finding SURF-08). Cross-referenced from D-135. Evidence: internal/core/aggregate/profile.go:95-101, internal/lint/posture.go:200-215, cmd/assent/run.go:533, schemas/policy/v1alpha1/profile.schema.json:25-28 |
| OQ-30 |
Is a pull_request-scoped CHANGELOG drift gate viable now that D-136 skips merge commits? The guard is retained with NO demonstrated reason — its original one is dead and its proposed successor measures false. D-125 skipped the gate on pull_request because refs/pull/N/merge's synthetic merge subject rendered into the generated changelog, so no committed CHANGELOG.md could match. D-136 killed that reason — that commit is a merge commit and is now skipped. The successor reason drafted in D-136's first version — "the merge ref also carries every commit landed on main since the branch forked, so the render is a union the branch's file cannot match, red by construction" — was then measured four ways and could not be made true: (1) PR #41's live refs/pull/41/merge (491bb2a, head 49eebb3 into base 7513d79) rendered with the new cliff.toml → verify-changelog: ok, 0 diff lines; (2) the direct counterexample — the same head merged into a main that had moved (1d8aa60, containing PR #40) → verify-changelog: ok, 0 diff lines, i.e. not red with the base moved; (3) a synthetic sandbox where base and lane each add a commit to the same cliff group and each regenerate → CONFLICT (content): Merge conflict in CHANGELOG.md, so the PR is unmergeable, GitHub mints no merge ref, and the gate never runs. (4) The strongest one, taken last and re-run rather than transcribed: GitHub RE-MINTED refs/pull/41/merge against the moved base after all of the above. Re-fetched live — 7715bf7, head ee5e527 into base 1d8aa60 — and put through the real gate script: verify-changelog: ok, 0 diff lines, 0 merge subjects rendered. That is not a simulation: it is the exact artifact a pull_request-scoped gate would evaluate, with the base moved past the fork point AND after the lane had merged main in — the direction the finding below shows is hazardous — and it is green. Measurement (1)'s 491bb2a at base 7513d79 is its stale predecessor, kept only to show the result did not depend on the base standing still. Mechanism the dead premise overlooked: the merge ref's CHANGELOG.md is not "the branch's committed file" — it is the three-way MERGE RESULT, which already contains the base's lines, because the file is merged like any other. So base movement ends in clean-and-matching or conflict-and-no-merge-ref. The third outcome EXISTS, and merge DIRECTION decides it — measured while writing this row. A clean textual auto-merge whose line order differs from git-cliff's topological order is red with no author error, and it reproduced immediately: merging origin/main into the lane (lane as first parent) auto-merged CHANGELOG.md without conflict and then failed verify-changelog on pure ordering — one docs(compare) line moved and PR #40's lines landed in a different position. The SAME two commits merged in the merge-ref direction (base 1d8aa60 as first parent, measurement (2) above) matched exactly. git-cliff's traversal follows parent order, so first-parent choice changes the render. This does not revive the retired premise — GitHub always mints the merge ref base-first, which is the direction that matched — but it means the clean-and-matching outcome is a property of that direction, measured on two merges, not a proof. It also re-confirms D-125's surviving rule: regenerate after any git merge origin/main. Still untested: behaviour on pull_request_target, on a PR from a fork, and after a force-push that re-mints the merge ref. Counter-evidence for enabling it: the only red reproduced on any merge ref was a branch that had not run task changelog-write for its own commits — a true positive the gate exists to catch, which argues the PR placement may now be correct rather than merely harmless. Correction, folded in from the PR #41 review because it belongs in the row and not only in a review thread: that review first read these greens as "the evidence points toward the PR gate being viable", and then took it back as one measurement short. The direction finding above supplies a false-positive mechanism it had not considered — a clean textual auto-merge whose line order differs from git-cliff's topological order reds with no author error and no author fix available. Four green measurements are therefore NOT a green light; on today's evidence the gate would not be enabled. Ruling needed (deliberately not taken here, operator's call): (a) enable the step on pull_request and delete the guard; (b) keep the guard and record the real reason once someone finds one; or (c) keep the guard permanently on cost/noise grounds and say so, rather than on a mechanism. Not to be resolved by deleting the guard on the strength of these three measurements alone — they show the claimed failure did not reproduce, not that no failure exists. |
Before any change to the pull_request guard on the changelog step in .github/workflows/verify.yaml; not a release blocker — the guard is fail-safe (the gate runs locally in task check and on push-to-main) |
Raised by the PR #41 review (finding CL-02) against D-136's first draft; measurements reproduced independently before recording. Sites now pointing here: Taskfile.yml check:, .github/workflows/verify.yaml, hack/release/README.md, hack/release/changelog_gate_test.sh §3. See D-125 and D-136 |
| OQ-31 |
May the GUARD-1 self-edit BLOCK path write a summary or supersession note, or is "zero forge writes on a self-modifying MR" absolute? If it is absolute, what channel carries the BLOCK to the human reviewer — given that no thread is posted and the exit code is 0? Raised by RELI-01 (D-138) and deliberately left UNDECIDED. The tension is real in both directions. For absolute: openspec/specs/p5-aud-audit-remediation/spec.md pins "the decision is BLOCK with zero forge writes (GUARD-1 dominance over the gap-degrade)" as a frozen acceptance criterion, and the guard exists so that an MR editing .assent/** cannot make assent vouch for its own policy — any write is a write the MR's own content influenced. Against absolute: the only human-visible surface then keeps whatever the previous run said, which today can be ✅ Decision: APPROVE, so the guard's output is invisible to the reviewer it protects, and D-130's compensating control (a REVIEW rerun upserts the summary and adds an unresolved discussion) does not reach this path because no thread is posted. Zero authority writes need not mean zero communication. Options, none taken here: (a) keep it absolute and carry BLOCK on a non-forge channel — a non-zero exit code, or a required CI job status; (b) permit exactly one write, a fixed-text supersession/BLOCK note with no policy-derived content, which cannot be steered by the MR; (c) permit the summary upsert but not the thread. (b) and (c) both reopen the frozen criterion above and need an openspec change proposal first — spec before code. Note that (a) changes an exit-code contract wrapper scripts rely on (docs/usage/cli.md), so it is not the free option it looks like. |
Before the RELI-01 fix lands (v0.2.1) |
Found by the 2026-08-09 audit's reliability lens; recorded in D-138. Evidence: cmd/assent/run.go step-9 GUARD switch, openspec/specs/p5-aud-audit-remediation/spec.md, openspec/specs/p5-e5-provider-host/spec.md REQ-E5-S08-03 |