diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml index 35b78b7e..f6216f98 100644 --- a/.github/workflows/deploy-cloudflare.yml +++ b/.github/workflows/deploy-cloudflare.yml @@ -31,9 +31,17 @@ jobs: # advanced while Test was still running, and passed only by coincidence # otherwise. `head_sha` alone is the commit that was tested, and it is what # the checkout below already uses. + # + # `head_branch` alone is not proof of main. A fork's pull request from a + # branch named `main` reports `head_branch == 'main'`, and its Test run + # concludes `success` because every job skips fork code. Only a push to + # this repository may deploy, or that fork's code would build with these + # secrets. if: >- (github.event_name == 'workflow_dispatch' && inputs.target != 'harness-proof') || (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main') runs-on: [self-hosted, linux, x64, harlan-desktop-deploy] timeout-minutes: 20 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97377d2e..3f27348a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,8 +18,17 @@ concurrency: group: test-${{ github.ref }} cancel-in-progress: true +# Every job runs on Harlan's workstation. A pull request from a fork must never +# reach it, because that would run untrusted code on the host. Each job +# therefore runs only for a push or for a pull request from a branch in this +# repository. A maintainer pushes a fork's change to a branch here to test it. +# +# Every job skipped still concludes the run as `success`. Deploy to Cloudflare +# guards on the triggering event and repository for that reason. + jobs: harness-proof: + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository runs-on: [self-hosted, linux, x64, harlan-desktop-ci] timeout-minutes: 15 steps: @@ -33,6 +42,7 @@ jobs: pnpm --filter @skilld/skill-harness-worker test:run harlan-desktop: + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository uses: harlan-zw/harlan-nuxt/.github/workflows/harlan-desktop-site-ci.yml@main with: # Ephemeral containers on the workstation. Safe here because the @@ -71,7 +81,7 @@ jobs: build: # Main builds in the deploy workflow. Keep the build check on pull requests. - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository runs-on: [self-hosted, linux, x64, harlan-desktop-ci] timeout-minutes: 30 steps: diff --git a/AGENTS.md b/AGENTS.md index 95414a37..218710b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,10 +62,9 @@ New Markdown at the repository root is an error. Identity and filters only. - **`skills.search` answers are frozen for skilld 3.2.0.** The CLI parses them with `deny_unknown_fields`, so one new field breaks every released `skilld search`. Add the field to `skills.get`. -- **`GOOGLE_RECOVERY.md` is in `.gitignore`.** Around ten source comments cite it as the reason a - surface is `noindex` or absent from the sitemap, and nobody cloning this repository can read it. - Treat those comments as pointing at nothing until the file is either committed or the comments - are rewritten to carry their own reason. +- **A comment carries its own reason.** Never cite a gitignored file, such as + `GOOGLE_RECOVERY.md`, or a file on a personal machine as the reason for a decision. + Nobody who clones this repository can read either one. ## Consumers diff --git a/app/pages/index.vue b/app/pages/index.vue index 4fa1e270..c46ccd7e 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -142,8 +142,8 @@ const heroVerbs = [ * `diagrams` was added and `research` was un-retired, both on these numbers. * See the header of clusters.ts. * - * Re-measure before trusting this order past October. Method and numbers: - * ~/scratch/notes/skilld-track-demand-2026-09-04.md + * Re-measure before trusting this order past October. The percentages + * below are each track's share of the weighted demand. */ const TRACK_DEMAND_ORDER = [ 'design', // 16.5% diff --git a/layers/marketing/app/pages/learn/[...slug].vue b/layers/marketing/app/pages/learn/[...slug].vue index 57f94db8..e5f09a9c 100644 --- a/layers/marketing/app/pages/learn/[...slug].vue +++ b/layers/marketing/app/pages/learn/[...slug].vue @@ -27,9 +27,9 @@ useSeoMeta({ ogTitle: () => data.value?.title ?? '', ogDescription: () => data.value?.description ?? '', ogUrl: canonicalUrl, - // The /learn index is a 55-word card list (GOOGLE_RECOVERY.md thin-page - // audit). Articles stay indexable; the index earns index back when it - // carries real editorial content. + // The /learn index is a 55-word card list, which the 2026-08-22 indexing + // audit flagged as a thin page. Articles stay indexable; the index earns + // index back when it carries real editorial content. robots: () => slug.value ? pageRobots(`/learn/${slug.value}`) : 'noindex,follow', }) diff --git a/layers/marketing/app/pages/skills/tag/[slug].vue b/layers/marketing/app/pages/skills/tag/[slug].vue index b25a599f..d0da0104 100644 --- a/layers/marketing/app/pages/skills/tag/[slug].vue +++ b/layers/marketing/app/pages/skills/tag/[slug].vue @@ -37,7 +37,8 @@ useSeoMeta({ description, ogTitle: title, ogDescription: description, - // Tag pages are noindex by default sitewide (GOOGLE_RECOVERY.md, 2026-08-22). + // Tag pages are noindex by default sitewide since 2026-08-22. They were + // auto-generated lists with no editorial text, and 16% of the sitemap. // Indexable only for a vocab tag with an editorial keep=1 decision. robots: () => (data.value?.indexable ? 'index,follow' : 'noindex,follow'), }) diff --git a/layers/registry/app/components/SkillDetail.vue b/layers/registry/app/components/SkillDetail.vue index de541e3b..8612461f 100644 --- a/layers/registry/app/components/SkillDetail.vue +++ b/layers/registry/app/components/SkillDetail.vue @@ -1045,9 +1045,8 @@ if (import.meta.server && pageState.value.status) { // 2026-08-22: the shared "A Claude Code skill for Cursor, Codex, and other // agents." suffix is gone. Identical boilerplate across 1,300+ meta -// descriptions was the last scaled-content signature in Google snippets -// (GOOGLE_RECOVERY.md). Provenance stays: "From owner/repo." carries the -// unique part. +// descriptions was the last scaled-content signature in Google snippets. +// Provenance stays: "From owner/repo." carries the unique part. function withSeoContext(text: string, owner: string, repo: string, max = 200): string { const suffix = ` From ${owner}/${repo}.` const collapsed = text.replace(/\s+/g, ' ').trim() diff --git a/layers/registry/server/data/clusters.ts b/layers/registry/server/data/clusters.ts index 34de6b6c..74f6cc71 100644 --- a/layers/registry/server/data/clusters.ts +++ b/layers/registry/server/data/clusters.ts @@ -74,8 +74,8 @@ * demand and costs a URL, while a homepage tile answers trending demand and * costs the scarcest space on the site. The grid caps at twelve, so the * subtraction happens there. `seo` and `coding` keep their pages and lose - * their tiles. Method and numbers: - * ~/scratch/notes/skilld-track-demand-2026-09-04.md + * their tiles. The method and the shares sit on TRACK_DEMAND_ORDER in + * app/pages/index.vue. * * 2026-08-25: `anti-slop-coding` joined as the coding counterpart. Its seven * pinned skills preserve the shared ranking and keep the page focused. It has diff --git a/nuxt.config.ts b/nuxt.config.ts index 1d37c69d..b1e25ddc 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -46,8 +46,8 @@ export default defineNuxtConfig({ '/_nuxt/v2/', ) }, - // 2026-08-22 agent lane (GOOGLE_RECOVERY.md): list the agent-only sitemap - // in llms.txt. The module's `notes` config exists but never renders, so + // 2026-08-22 agent lane, where agents discover pages without Google's + // sitemap: list the agent-only sitemap in llms.txt. The module's `notes` config exists but never renders, so // this pushes a link into the first section instead. 'ai-ready:llms-txt': (payload: { sections?: { links?: { title: string, href: string, description?: string }[] }[], notes: string[] }) => { const section = payload.sections?.[0] @@ -298,7 +298,6 @@ export default defineNuxtConfig({ runtimeSync: true, // llms.txt is an index, not a dump: each entry links the page's .md. // The 28.5 MB llms-full.txt inline dump is retired below (routeRules). - // GOOGLE_RECOVERY.md, agent lane rework. llmsTxt: { markdownLinks: true, }, @@ -811,9 +810,9 @@ export default defineNuxtConfig({ }, sitemap: { - // `/learn` index is a 55-word card list, noindex since 2026-08-22 - // (GOOGLE_RECOVERY.md). Articles stay indexable and sitemap-listed; only - // the bare index leaves. Global so no child sitemap can re-adopt it. + // `/learn` index is a 55-word card list, noindex since 2026-08-22 as a + // thin page. Articles stay indexable and sitemap-listed; only the bare + // index leaves. Global so no child sitemap can re-adopt it. exclude: ['/learn'], sitemaps: { pages: { diff --git a/server/middleware/trailing-slash.ts b/server/middleware/trailing-slash.ts index 6860a36a..0d3fd1ba 100644 --- a/server/middleware/trailing-slash.ts +++ b/server/middleware/trailing-slash.ts @@ -1,5 +1,4 @@ -// 2026-08-22: canonical trailing-slash policy (GOOGLE_RECOVERY.md technical -// audit). Nuxt served 200 on both `/gh/x/y` and `/gh/x/y/`, and canonicals +// 2026-08-22: canonical trailing-slash policy, from a technical SEO audit. Nuxt served 200 on both `/gh/x/y` and `/gh/x/y/`, and canonicals // point at the no-slash form, so Google registered duplicate URLs (37 pages // in "Duplicate, Google chose different canonical"). One URL per page: // strip the trailing slash with a 301. diff --git a/server/routes/ai-sitemap.xml.get.ts b/server/routes/ai-sitemap.xml.get.ts index 6f782ce5..b91885e6 100644 --- a/server/routes/ai-sitemap.xml.get.ts +++ b/server/routes/ai-sitemap.xml.get.ts @@ -1,6 +1,6 @@ /// -// 2026-08-22 agent lane (GOOGLE_RECOVERY.md): an agent-only sitemap listing +// 2026-08-22 agent lane: an agent-only sitemap listing // every AI-ready page, including pages that are noindex for Google. Referenced // from llms.txt only, never robots.txt, so it cannot couple agent discovery to // the Google recovery surface. Google officially ignores llms.txt, and even if