From 4e66a6ba7c57decb24d421128f113b92d5d2e92d Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Mon, 7 Sep 2026 15:42:13 +0530 Subject: [PATCH 1/2] docs: add graycode ecosystem integrity implementation plan Plan for the four workstreams (skills registry, cloud wire contract, router boundary, naming sweep) that this session executed across the four graycode repos. Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k --- .../2026-09-05-graycode-eco-integrity.md | 1781 +++++++++++++++++ 1 file changed, 1781 insertions(+) create mode 100644 docs/plans/2026-09-05-graycode-eco-integrity.md diff --git a/docs/plans/2026-09-05-graycode-eco-integrity.md b/docs/plans/2026-09-05-graycode-eco-integrity.md new file mode 100644 index 00000000..3ea3c82c --- /dev/null +++ b/docs/plans/2026-09-05-graycode-eco-integrity.md @@ -0,0 +1,1781 @@ +# GrayCode Ecosystem Integrity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Repair the three broken links between the four graycode repos (skills registry, cloud wire contract, router boundary) and remove legacy `hawk`/`starling`/`eagle` naming from every user-visible surface. + +**Architecture:** Four independent workstreams, each shippable as its own PR stack. A fixes the graycode-cli → graycode-skills registry link at all three broken layers (publishing, JSON shape, install discovery). B renames the cloud wire contract from `hawk` to `graycode` in graycode-platform, which makes the already-correct CLI client work. C aligns the documented router boundary with the enforced one and repairs two defective guards. D is a mechanical naming sweep. + +**Tech Stack:** Go 1.26+ (graycode-cli, graycode-router), Python 3.11 + pytest (graycode-skills), TypeScript + Hono + vitest + Cloudflare D1 (graycode-platform), GitHub Actions. + +**Spec:** This document is self-contained. Its findings were produced by a read-only scouting pass over all four repos on 2026-09-05 and verified by an independent adversarial pass; every claim below carries a `file:line` citation. + +## Global Constraints + +- **Branch discipline (all four repos).** Never commit to `main`. Create a feature branch first, named `feat/`, `fix/` or `chore/`. Open a PR, get CI green, then merge. Source: `graycode-cli/AGENTS.md:16`, `graycode-skills/AGENTS.md:13`, `graycode-platform/AGENTS.md:13`. +- **graycode-cli is currently on branch `chore/compat-matrix-0-0-1`, not `main`.** Branch from `main` for this work, not from the current HEAD. +- **Commits:** Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`). No `Co-authored-by:` trailers in graycode-router; a githook strips them. Source: `graycode-router/AGENTS.md:563`. +- **CHANGELOG:** every repo follows Keep a Changelog. Add entries under `## [Unreleased]` in the repo you touch. graycode-cli uses Keep a Changelog 1.1.0, graycode-router and graycode-skills use 1.0.0. +- **Go:** Go 1.26+, pure Go, no CGO. `gofumpt` formatting is enforced in CI. Table-driven tests. +- **graycode-cli CI gate:** `make ci` runs `tidy fmt vet boundaries lint test-race security api-validate` (`graycode-cli/Makefile:160`). The `boundaries` target aggregates nine guard scripts (`Makefile:136`). +- **graycode-platform CI gate:** `pnpm --filter @graycode/worker check` runs `prettier --check . && tsc --noEmit && vitest run`. Repo is pinned to pnpm 9.15.0 via Corepack. +- **Load-bearing identifiers that must NOT be renamed in any workstream.** These are wire values, storage keys, or third-party contracts, not prose: + - `hwc_` device-token prefix (`graycode-platform/apps/worker/src/domain/tokens.ts:18`) + - `HAWK_CLOUD` service binding and `HawkCloudService` RPC entrypoint (`apps/bff/wrangler.jsonc:21,23`) + - OTel metric names `hawk.window_days`, `hawk.sessions`, `hawk.tokens.total` (`apps/worker/src/routes/analytics.ts:532,540,555`) + - `HAWK_CONFIG_DIR` env fallback (`graycode-router/config/provider_env.go:402`) + - `~/.hawk/{env,.env,.legacy-env-migrated}` migration source paths (`graycode-router/credentials/migrate.go:14,88`) + - `hawk_build` / `hawk_build_concise` ToolNamespace wire values (`graycode-router/tools/versioning.go:114,115`) + - `/hawk:` plugin invoke prefix (`graycode-skills/tools/sync_marketplace.py:65`) and the `hawk` value in `AGENT_ENUM` (`graycode-skills/tools/validate_skill.py:66`) + - `hawk-progressive-disclosure` marker written into existing SKILL.md files (`graycode-skills/tools/migrate_oversized_skills.py:30`) + - GitNexus index names inside `` blocks in every AGENTS.md / CLAUDE.md — these are regenerated by the tool + - Go module paths `github.com/GrayCodeAI/{harrier,kestrel,merlin,shrike,swift,falcon}` — these are **live** dependencies in `graycode-cli/go.mod:15-18,55-57,172`, not dead names + - The `/products/` URL slugs in `graycode-platform/apps/web/lib/products.ts:9` +- **Workstream B is the only one that changes a deployed wire contract.** It requires a D1 migration and a coordinated worker deploy. Do not merge B's worker PR without applying migration `0024` first. + +--- + +# Workstream A — Skills Registry + +**Problem.** `graycode skills search` cannot work today, and `graycode skills install GrayCodeAI/graycode-skills ` cannot work either. The link is broken at three independent layers, and fixing only the URL restores nothing. + +1. **No public URL exists.** `graycode-skills/.github/workflows/publish-registry.yml:55-62` generates `registry.json`, signs it, then uploads it only as a GitHub Actions artifact (90-day retention). There is no release, no Pages, no R2, no commit-back. `registry.json` is gitignored at `graycode-skills/.gitignore:50`, so `raw.githubusercontent.com` returns 404 under both the old `starling` name and the new one. Verified: both URLs return HTTP 404; `GrayCodeAI/starling` 301-redirects to `GrayCodeAI/graycode-skills`; the repo has `has_pages=false` and its only release `v0.1.0` has zero assets. +2. **The JSON shapes do not match.** `tools/update_registry.py:165` emits a bare JSON array. `graycode-cli/internal/plugin/registry.go:73-77` parses an object `{version, updated_at, skills[]}`. `json.Unmarshal` of an array into that struct fails, so `FetchIndex` returns `invalid index` (`registry.go:136-137`) even with a working URL. The emitter also omits `repo`, which `auto_skill.go:175` needs to build the clone URL. +3. **Install cannot find the skills.** `registry.go:261-265` scans only `/*/SKILL.md` or `/skills/*/SKILL.md`. graycode-skills stores skills at `categories///SKILL.md` and has no `skills/` directory, so every install returns `skill not found`. + +**Decision (approved):** publish `registry.json` as an asset on a rolling GitHub Release. Stable URL, no git bloat, no new infrastructure. + +**Also in scope.** `graycode-cli/internal/plugin/marketplace.go:49` points at `plugins-registry.json`, a file that **nothing in any of the four repos generates**. It is a phantom default source and is removed here. + +--- + +### Task A1: Emit the registry shape graycode-cli actually parses + +**Files:** +- Modify: `graycode-skills/tools/update_registry.py:115-124` (add `repo`), `:160-166` (wrap in object) +- Modify: `graycode-skills/tools/registry_schema.py:30-58` (allow `repo`), `:84-87` (`REGISTRY_SCHEMA` becomes an object), `:217-230` (`load_and_validate` reads the object) +- Test: `graycode-skills/tests/test_update_registry.py` + +**Interfaces:** +- Produces: on-disk `registry.json` of the form `{"version": 1, "skills": [entry, ...]}` where each entry gains `"repo": "GrayCodeAI/graycode-skills"`. Task A3 relies on this shape. `build_registry()` still returns the bare `list[dict]` so `tools/skill_graph.py:13,268` is unaffected. + +> **Determinism matters.** `update_registry.py --check` compares generated text against the file on disk and is run in CI. Do **not** add a wall-clock `updated_at`; it would make `--check` fail on every run. The top-level `updated_at` is omitted entirely. `graycode-cli` never reads `idx.Version` or `idx.UpdatedAt` (verified: zero references outside tests), so an absent `updated_at` is harmless. + +- [ ] **Step 1: Write the failing tests** + +Append to `graycode-skills/tests/test_update_registry.py`: + +```python +class TestCanonicalRenderShape: + """registry.json must match the object shape graycode-cli parses.""" + + def test_render_wraps_entries_in_object(self): + from update_registry import render_registry + + doc = json.loads(render_registry([{"name": "a", "description": "d"}])) + assert isinstance(doc, dict), "top level must be an object, not an array" + assert doc["version"] == 1 + assert doc["skills"] == [{"name": "a", "description": "d"}] + + def test_render_omits_updated_at_for_determinism(self): + from update_registry import render_registry + + first = render_registry([{"name": "a"}]) + second = render_registry([{"name": "a"}]) + assert first == second + assert "updated_at" not in json.loads(first) + + def test_entries_carry_repo_slug(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + from update_registry import build_registry + + cat = tmp_path / "categories" / "python" / "demo-skill" + cat.mkdir(parents=True) + (cat / "SKILL.md").write_text( + "---\nname: demo-skill\ndescription: A demo skill\n---\n\nBody\n" + ) + monkeypatch.setattr("update_registry.REPO_ROOT", tmp_path) + monkeypatch.setattr("update_registry.CATEGORIES_DIR", tmp_path / "categories") + + entries = build_registry() + assert entries[0]["repo"] == "GrayCodeAI/graycode-skills" + + def test_schema_accepts_repo_field(self): + from registry_schema import validate_registry_entry + + errors = validate_registry_entry( + { + "name": "demo", + "description": "d", + "category": "python", + "tags": ["python"], + "path": "categories/python/demo", + "file_count": 1, + "has_scripts": False, + "repo": "GrayCodeAI/graycode-skills", + }, + path="demo", + ) + assert errors == [] +``` + +Ensure `import json` and `from pathlib import Path` are present at the top of the file. + +- [ ] **Step 2: Run the tests to verify they fail** + +```bash +cd graycode-skills && python -m pytest tests/test_update_registry.py -k CanonicalRenderShape -v +``` + +Expected: FAIL. `test_render_wraps_entries_in_object` fails with `assert isinstance(doc, dict)` because `render_registry` currently returns a JSON array. `test_entries_carry_repo_slug` fails with `KeyError: 'repo'`. `test_schema_accepts_repo_field` fails with an `additionalProperties` violation naming `repo`. + +- [ ] **Step 3: Add the `repo` slug to each entry** + +In `graycode-skills/tools/update_registry.py`, add a module constant next to the other module-level paths (near `REGISTRY_PATH`, around line 24): + +```python +# The GitHub slug every skill in this repo is installed from. graycode-cli +# builds its clone URL from this field (internal/plugin/auto_skill.go). +REGISTRY_REPO = "GrayCodeAI/graycode-skills" +``` + +Then extend the entry literal at line 115: + +```python + entry = { + "name": name, + "description": description, + "category": category_name, + "tags": tags, + "path": path, + "repo": REGISTRY_REPO, + "file_count": count_files(skill_dir), + "has_scripts": has_scripts_dir(skill_dir), + } +``` + +- [ ] **Step 4: Wrap the rendered document in the object shape** + +Replace `render_registry` in `graycode-skills/tools/update_registry.py`: + +```python +def render_registry(entries: list[dict]) -> str: + """Render registry entries in the canonical on-disk format. + + The top level is an object, not an array: graycode-cli parses + {version, updated_at, skills[]} (internal/plugin/registry.go). No + timestamp is emitted so that `--check` stays deterministic. + """ + document = {"version": 1, "skills": entries} + return json.dumps(document, indent=2, ensure_ascii=False) + "\n" +``` + +- [ ] **Step 5: Teach the schema about the new shape** + +In `graycode-skills/tools/registry_schema.py`, add `repo` to `REGISTRY_ENTRY_SCHEMA["properties"]` alongside the other optional fields (the block ending at line 78, before `"additionalProperties": False`): + +```python + "repo": { + "type": "string", + "description": "GitHub owner/repo slug the skill is installed from", + }, +``` + +Replace `REGISTRY_SCHEMA` (line 84): + +```python +REGISTRY_SCHEMA: dict[str, Any] = { + "type": "object", + "properties": { + "version": {"type": "integer"}, + "skills": {"type": "array", "items": REGISTRY_ENTRY_SCHEMA}, + }, + "required": ["version", "skills"], + "additionalProperties": False, +} +``` + +Then update `load_and_validate` (line 217) so it validates the `skills` array rather than the whole document as an array. Read the function first and adapt its existing error-collection style; the only change is that the list of entries is now `data["skills"]` instead of `data`, and a non-object top level is itself an error. + +- [ ] **Step 6: Run the tests to verify they pass** + +```bash +cd graycode-skills && python -m pytest tests/test_update_registry.py -v && python -m pytest -q +``` + +Expected: PASS, whole suite green. + +- [ ] **Step 7: Verify the real corpus still generates and validates** + +```bash +cd graycode-skills +python tools/update_registry.py +python -c "import json; d=json.load(open('registry.json')); print(type(d).__name__, d['version'], len(d['skills']), d['skills'][0]['repo'])" +python tools/update_registry.py --check && echo "CHECK CLEAN (deterministic)" +rm registry.json +``` + +Expected: `dict 1 12167 GrayCodeAI/graycode-skills`, then `CHECK CLEAN (deterministic)`. Delete the generated file; it stays gitignored. + +- [ ] **Step 8: Commit** + +```bash +git add tools/update_registry.py tools/registry_schema.py tests/test_update_registry.py +git commit -m "fix: emit registry.json in the object shape graycode-cli parses + +The generator emitted a bare JSON array while graycode-cli parses +{version, updated_at, skills[]}, so FetchIndex failed with 'invalid +index' regardless of URL. Entries now also carry the repo slug that +the installer needs to build a clone URL. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task A2: Publish the registry to a stable public URL + +**Files:** +- Modify: `graycode-skills/.github/workflows/publish-registry.yml:11-14` (add `permissions`), `:55-62` (add the release step after the artifact upload) + +**Interfaces:** +- Produces: `https://github.com/GrayCodeAI/graycode-skills/releases/latest/download/registry.json`. Task A3 hard-codes this URL. + +> **Why a rolling release and not `latest`.** `releases/latest/download/` resolves to the most recent **non-prerelease** release. The existing `v0.1.0` release is currently the latest, so the new rolling release must be created as a normal (non-draft, non-prerelease) release for that URL to resolve to it. Tagging it `registry-latest` and re-uploading with `--clobber` keeps exactly one moving target. + +- [ ] **Step 1: Grant the workflow permission to write releases** + +In `graycode-skills/.github/workflows/publish-registry.yml`, add a `permissions` block to the `build-and-publish` job, directly under `runs-on`: + +```yaml +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: write + steps: +``` + +- [ ] **Step 2: Add the release-publishing step** + +Append to the end of the same file, after the existing `Upload registry artifacts` step: + +```yaml + - name: Publish registry to the rolling release + if: github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # One moving release holds the current registry. The Actions + # artifact above is retained separately for 90-day forensics. + if ! gh release view registry-latest >/dev/null 2>&1; then + gh release create registry-latest \ + --title "Skill registry (rolling)" \ + --notes "Generated registry.json for the current main. Updated automatically; do not delete." \ + --latest=false + fi + gh release upload registry-latest \ + registry.json registry-signature.json --clobber +``` + +`--latest=false` keeps the rolling release from displacing real version tags in the GitHub UI. The download URL used by the CLI in Task A3 addresses the tag directly, so it does not depend on which release is marked latest. + +- [ ] **Step 3: Validate the workflow file parses** + +```bash +cd graycode-skills && python -c "import yaml,sys; d=yaml.safe_load(open('.github/workflows/publish-registry.yml')); j=d['jobs']['build-and-publish']; print('permissions:', j['permissions']); print('steps:', [s['name'] for s in j['steps']])" +``` + +Expected: `permissions: {'contents': 'write'}` and a step list ending with `Publish registry to the rolling release`. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/publish-registry.yml +git commit -m "feat: publish registry.json to a rolling GitHub release + +The registry had no public URL: CI only uploaded it as a 90-day +Actions artifact and the file is gitignored, so every raw +githubusercontent URL 404d. A rolling registry-latest release gives +the CLI a stable download target without putting a 4.3 MB generated +file into git history. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +- [ ] **Step 5: After merge, confirm the URL is live** + +```bash +curl -sIL https://github.com/GrayCodeAI/graycode-skills/releases/download/registry-latest/registry.json | grep -E '^HTTP' +``` + +Expected: a final `HTTP/2 200`. If the workflow has not run since merge, trigger it with `gh workflow run publish-registry.yml -R GrayCodeAI/graycode-skills` and re-check. **Task A3 cannot be verified end-to-end until this returns 200.** + +--- + +### Task A3: Point graycode-cli at the published registry and drop the phantom marketplace source + +**Files:** +- Modify: `graycode-cli/internal/plugin/registry.go:20` +- Modify: `graycode-cli/internal/plugin/marketplace.go:42-50` +- Test: `graycode-cli/internal/plugin/registry_test.go`, `graycode-cli/internal/plugin/marketplace_test.go` + +**Interfaces:** +- Consumes: the object shape from Task A1 and the URL from Task A2. +- Produces: `defaultIndexURL` pointing at the rolling release; `defaultMarketplaceSources()` returning an empty slice. + +> **Why the marketplace source is removed rather than repointed.** `plugins-registry.json` is generated by nothing in any of the four repos; the only reference anywhere is `marketplace.go:49`. `FetchAll` (`marketplace.go:111-132`) returns `(nil, nil)` when there are no sources and no errors, so removing the dead source turns a confusing fetch failure into a clean empty list. Users add real sources with `graycode plugin marketplace add`. + +- [ ] **Step 1: Write the failing tests** + +Append to `graycode-cli/internal/plugin/registry_test.go`: + +```go +func TestDefaultIndexURLIsPublished(t *testing.T) { + const want = "https://github.com/GrayCodeAI/graycode-skills/releases/download/registry-latest/registry.json" + if defaultIndexURL != want { + t.Fatalf("defaultIndexURL = %q, want %q", defaultIndexURL, want) + } + if strings.Contains(defaultIndexURL, "starling") { + t.Errorf("defaultIndexURL still references the renamed starling repo") + } +} + +func TestFetchIndexParsesGeneratedShape(t *testing.T) { + // Byte-for-byte the shape graycode-skills/tools/update_registry.py emits. + const generated = `{ + "version": 1, + "skills": [ + { + "name": "ab-test-setup", + "description": "Plan and design an A/B test", + "category": "testing", + "tags": ["testing"], + "path": "categories/testing/ab-test-setup", + "repo": "GrayCodeAI/graycode-skills", + "file_count": 1, + "has_scripts": false + } + ] +} +` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(generated)) + })) + defer srv.Close() + + rc := &RegistryClient{IndexURL: srv.URL, CacheDir: t.TempDir(), client: srv.Client()} + idx, err := rc.FetchIndex() + if err != nil { + t.Fatalf("FetchIndex: %v", err) + } + if len(idx.Skills) != 1 { + t.Fatalf("skills = %d, want 1", len(idx.Skills)) + } + if idx.Skills[0].Repo != "GrayCodeAI/graycode-skills" { + t.Errorf("Repo = %q, want the slug the installer clones from", idx.Skills[0].Repo) + } +} +``` + +Append to `graycode-cli/internal/plugin/marketplace_test.go`: + +```go +func TestNoPhantomDefaultMarketplaceSource(t *testing.T) { + for _, src := range DefaultMarketplaceSources() { + if strings.Contains(src.URL, "plugins-registry.json") { + t.Fatalf("default source %q points at plugins-registry.json, which nothing generates", src.Name) + } + } +} + +func TestFetchAllWithNoSourcesReturnsEmptyNotError(t *testing.T) { + mc := &MarketplaceClient{Sources: nil, CacheDir: t.TempDir()} + entries, err := mc.FetchAll() + if err != nil { + t.Fatalf("FetchAll with no sources returned error: %v", err) + } + if len(entries) != 0 { + t.Fatalf("entries = %d, want 0", len(entries)) + } +} +``` + +Ensure `net/http`, `net/http/httptest` and `strings` are imported in each file. `MarketplaceClient` construction must match the real struct; read `marketplace.go` and adjust the literal if the field set differs. + +- [ ] **Step 2: Run the tests to verify they fail** + +```bash +cd graycode-cli && go test ./internal/plugin/ -run 'TestDefaultIndexURLIsPublished|TestFetchIndexParsesGeneratedShape|TestNoPhantomDefaultMarketplaceSource|TestFetchAllWithNoSourcesReturnsEmptyNotError' -v +``` + +Expected: FAIL. The URL test reports the `starling` constant. The shape test passes only if the Go struct already matches, which it does, so it should pass once the URL is fixed; if it fails, the emitter in A1 drifted. The marketplace test reports the `plugins-registry.json` default. + +- [ ] **Step 3: Repoint the registry index URL** + +In `graycode-cli/internal/plugin/registry.go`, replace line 20: + +```go +// defaultIndexURL is the rolling release asset published by +// graycode-skills/.github/workflows/publish-registry.yml. The registry is a +// generated 4.3 MB artifact and is deliberately not committed to that repo, +// so a raw.githubusercontent.com URL cannot work. +const defaultIndexURL = "https://github.com/GrayCodeAI/graycode-skills/releases/download/registry-latest/registry.json" +``` + +- [ ] **Step 4: Remove the phantom marketplace source** + +In `graycode-cli/internal/plugin/marketplace.go`, replace the exported `DefaultMarketplaceSources` function at lines 43-52 (keep the name and the exported signature; `NewMarketplaceClient` and the `plugin marketplace` commands already call it): + +```go +// DefaultMarketplaceSources returns the built-in plugin index sources. +// +// There are none. No repository in the GrayCode ecosystem generates a +// plugins-registry.json, so shipping a built-in source only produced a 404 +// on every `graycode plugin marketplace list`. Users register real sources +// with `graycode plugin marketplace add `. +func DefaultMarketplaceSources() []MarketplaceSource { + return nil +} +``` + +Leave `loadUserMarketplaceSources` and `SaveUserSources` untouched. + +- [ ] **Step 5: Run the tests to verify they pass** + +```bash +cd graycode-cli && go test ./internal/plugin/ -v +``` + +Expected: PASS, whole package green. Pre-existing tests that use `"GrayCodeAI/starling"` as row **data** (`registry_test.go:20,21,167,192,250,276,285`, `skillslock_test.go:20`) are fixtures, not URL pins; leave them for Task D4. + +- [ ] **Step 6: Verify against the live registry** + +Only runnable once Task A2's URL returns 200. + +```bash +cd graycode-cli && go build -o /tmp/graycode ./cmd/graycode && /tmp/graycode skills search testing | head -20 +``` + +Expected: a list of matching skills. If it prints a registry error, re-check Step 5 of Task A2. + +- [ ] **Step 7: Commit** + +```bash +git add internal/plugin/registry.go internal/plugin/marketplace.go internal/plugin/registry_test.go internal/plugin/marketplace_test.go +git commit -m "fix: point the skill index at the published registry release + +The index URL referenced GrayCodeAI/starling, a repo renamed to +graycode-skills whose registry.json is generated and never committed, +so the URL 404d under either name. It now reads the rolling release +asset. The built-in marketplace source pointed at a +plugins-registry.json that nothing generates and is removed. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task A4: Make install discover skills at any repository layout + +**Files:** +- Modify: `graycode-cli/internal/plugin/registry.go:258-270` (the discovery block inside `Install`) +- Test: `graycode-cli/internal/plugin/registry_test.go` + +**Interfaces:** +- Consumes: nothing from earlier tasks; independently testable. +- Produces: `discoverSkillDirs(root string) (map[string]string, error)` mapping skill name to the directory containing its `SKILL.md`. `Install` iterates this map instead of `os.ReadDir(skillsRoot)`. + +> **Root cause, not the reported symptom.** The reported failure is "installing from graycode-skills says skill not found". The cause is that discovery hard-codes two layouts (`//` and `/skills//`). Every repo with any other layout fails the same way. One bounded walk fixes all of them, and is a smaller diff than adding a third special case. + +- [ ] **Step 1: Write the failing test** + +Append to `graycode-cli/internal/plugin/registry_test.go`: + +```go +func TestDiscoverSkillDirs(t *testing.T) { + tests := []struct { + name string + layout []string // SKILL.md paths relative to the repo root + want []string // expected skill names + }{ + { + name: "flat layout", + layout: []string{"go-review/SKILL.md"}, + want: []string{"go-review"}, + }, + { + name: "agentskills.io skills/ layout", + layout: []string{"skills/go-review/SKILL.md"}, + want: []string{"go-review"}, + }, + { + name: "graycode-skills categories layout", + layout: []string{"categories/go/go-review/SKILL.md", "categories/python/pandas/SKILL.md"}, + want: []string{"go-review", "pandas"}, + }, + { + name: "ignores vendored and dot directories", + layout: []string{"go-review/SKILL.md", ".git/hooks/SKILL.md", "node_modules/pkg/SKILL.md"}, + want: []string{"go-review"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + for _, rel := range tc.layout { + full := filepath.Join(root, rel) + if err := os.MkdirAll(filepath.Dir(full), 0o750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, []byte("---\nname: x\n---\n"), 0o600); err != nil { + t.Fatal(err) + } + } + + got, err := discoverSkillDirs(root) + if err != nil { + t.Fatalf("discoverSkillDirs: %v", err) + } + if len(got) != len(tc.want) { + t.Fatalf("found %d skills %v, want %d %v", len(got), keysOf(got), len(tc.want), tc.want) + } + for _, name := range tc.want { + dir, ok := got[name] + if !ok { + t.Errorf("missing skill %q; got %v", name, keysOf(got)) + continue + } + if _, err := os.Stat(filepath.Join(dir, "SKILL.md")); err != nil { + t.Errorf("skill %q maps to %q which has no SKILL.md", name, dir) + } + } + }) + } +} + +func keysOf(m map[string]string) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out +} +``` + +Ensure `os`, `path/filepath` and `sort` are imported. + +- [ ] **Step 2: Run the test to verify it fails** + +```bash +cd graycode-cli && go test ./internal/plugin/ -run TestDiscoverSkillDirs -v +``` + +Expected: FAIL to compile with `undefined: discoverSkillDirs`. + +- [ ] **Step 3: Implement bounded discovery** + +Add to `graycode-cli/internal/plugin/registry.go`: + +```go +// maxSkillSearchDepth bounds how deep discoverSkillDirs walks below the repo +// root. graycode-skills nests skills at categories///, which +// is depth 3; anything deeper is almost certainly test data or a vendored +// copy. +const maxSkillSearchDepth = 4 + +// discoverSkillDirs finds every directory under root containing a SKILL.md, +// keyed by the directory name. It replaces the previous two hard-coded +// layouts (// and /skills//) so repositories that +// group skills under a category directory are installable too. +// +// On a duplicate skill name the shallowest path wins; ties keep the first +// lexicographic match so the result is deterministic. +func discoverSkillDirs(root string) (map[string]string, error) { + found := map[string]string{} + depthOf := map[string]int{} + + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, relErr := filepath.Rel(root, path) + if relErr != nil { + return nil //nolint:nilerr // an unrelatable path is simply skipped + } + if d.IsDir() { + if path == root { + return nil + } + name := d.Name() + if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" { + return filepath.SkipDir + } + if len(strings.Split(filepath.ToSlash(rel), "/")) > maxSkillSearchDepth { + return filepath.SkipDir + } + return nil + } + if d.Name() != "SKILL.md" { + return nil + } + dir := filepath.Dir(path) + if dir == root { + return nil // a top-level SKILL.md documents the repo, not a skill + } + name := filepath.Base(dir) + depth := len(strings.Split(filepath.ToSlash(rel), "/")) + if prev, ok := found[name]; ok { + if depthOf[name] <= depth { + return nil + } + _ = prev + } + found[name] = dir + depthOf[name] = depth + return nil + }) + if err != nil { + return nil, fmt.Errorf("scan skills: %w", err) + } + return found, nil +} +``` + +Add `"io/fs"` to the imports. + +- [ ] **Step 4: Run the test to verify it passes** + +```bash +cd graycode-cli && go test ./internal/plugin/ -run TestDiscoverSkillDirs -v +``` + +Expected: PASS, all four subtests. + +- [ ] **Step 5: Wire it into `Install`** + +In `graycode-cli/internal/plugin/registry.go`, replace the discovery block at lines 258-270. Delete the `skillsRoot` computation and the `os.ReadDir(skillsRoot)` call, and drive the existing loop from the map instead: + +```go + // Discover skills in the cloned repo, whatever layout it uses. + discovered, err := discoverSkillDirs(tmpDir) + if err != nil { + return "", err + } + names := make([]string, 0, len(discovered)) + for name := range discovered { + names = append(names, name) + } + sort.Strings(names) +``` + +Then change the loop header from `for _, e := range entries {` to `for _, name := range names {`, delete the `if !e.IsDir() { continue }` guard and the `name := e.Name()` line, and change the `srcSkill` assignment to: + +```go + srcSkill := filepath.Join(discovered[name], "SKILL.md") +``` + +Leave the rest of the loop body, including the `skillName` filter, the trust checks, and the lockfile writes, exactly as they are. Add `"sort"` to the imports if it is not already present. + +> `// ponytail: whole-repo shallow clone. Installing one skill from graycode-skills clones ~127 MB of categories. Switch to git sparse-checkout of the skill's indexed path if install latency becomes a complaint.` + +Add that comment above the `git clone` call at line 247. + +- [ ] **Step 6: Run the full package and the boundary guards** + +```bash +cd graycode-cli && go test ./internal/plugin/ -v && gofumpt -l internal/plugin/ && make boundaries +``` + +Expected: package PASS, `gofumpt -l` prints nothing, all nine guards pass. + +- [ ] **Step 7: Verify a real install end-to-end** + +```bash +cd graycode-cli && go build -o /tmp/graycode ./cmd/graycode && /tmp/graycode skills install GrayCodeAI/graycode-skills ab-test-setup +``` + +Expected: reports the skill installed. Before this change it returned `skill "ab-test-setup" not found`. + +- [ ] **Step 8: Commit** + +```bash +git add internal/plugin/registry.go internal/plugin/registry_test.go +git commit -m "fix: discover skills at any repository layout on install + +Install scanned only //SKILL.md and +/skills//SKILL.md, so every repo grouping skills under a +category directory - graycode-skills included - reported 'skill not +found'. A bounded walk replaces both special cases. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +# Workstream B — Cloud Wire Contract + +**Problem.** Two field names drifted between graycode-cli and graycode-platform. Both schemas on the worker are `.strict()` zod objects, so both calls are rejected outright. + +| Call | CLI sends | Worker requires | Result | +|---|---|---|---| +| `POST /v1/auth/device/start` | `graycodeVersion` (`client.go:105`) | `hawkVersion` (`validation.ts:30`, `auth.ts:32`) | 400, `graycode cloud login` can never succeed | +| `POST /v1/usage` | `capability: "graycode"` (`exec.go:373`) | enum `[hawk, swift, shrike, harrier, merlin, kestrel]` (`contracts/v1.ts:8-15`) | 400, silently discarded because `RecordUsage` is fail-open (`client.go:176-179`) | + +**Decision (approved):** rename the wire to `graycode`. The CLI is already correct, so **graycode-cli needs no production change in this workstream** beyond one test fixture. All edits are in graycode-platform. + +**Deployment order is not optional.** Migration `0024` must be applied to D1 before the worker deploy, or every device read breaks on a missing column. + +> **Note on migration numbering.** `apps/worker/migrations/` already contains a collision: both `0022_graph_ledger.sql` and `0022_identity_ui.sql` exist. The next free prefix is `0024`, since `0023_usage_outbox.sql` is taken. Do not reuse `0022` or `0023`. + +--- + +### Task B1: Migrate the D1 schema and backfill legacy rows + +**Files:** +- Create: `graycode-platform/apps/worker/migrations/0024_graycode_capability_rename.sql` + +**Interfaces:** +- Produces: column `devices.graycode_version` (was `hawk_version`), column `cli_device_authorizations.graycode_version` (was `hawk_version`), and every `capability = 'hawk'` row rewritten to `'graycode'`. Tasks B2 and B3 read these names. + +- [ ] **Step 1: Write the migration** + +Create `graycode-platform/apps/worker/migrations/0024_graycode_capability_rename.sql`: + +```sql +-- Rename the CLI wire fields from the legacy hawk product name to graycode. +-- +-- The CLI has always sent `graycodeVersion` and `capability: "graycode"` +-- (graycode-cli internal/platform/cloud/client.go, cmd/exec.go). The worker's +-- strict zod schemas required `hawkVersion` and rejected the `graycode` +-- capability, so device login returned 400 and every usage event was dropped. +-- The wire contract moves to the name the product actually has. +-- +-- SQLite supports RENAME COLUMN from 3.25; D1 is well past that. + +ALTER TABLE devices RENAME COLUMN hawk_version TO graycode_version; +ALTER TABLE cli_device_authorizations RENAME COLUMN hawk_version TO graycode_version; + +-- Backfill rows written while the enum still said 'hawk'. The capability +-- column is free TEXT, so historical rows would otherwise fail validation on +-- any read path that re-parses them. +UPDATE usage_events SET capability = 'graycode' WHERE capability = 'hawk'; +UPDATE sessions SET capability = 'graycode' WHERE capability = 'hawk'; +``` + +- [ ] **Step 2: Verify the migration applies against a scratch database** + +```bash +cd graycode-platform/apps/worker +npx wrangler d1 migrations list graycode-cloud --local +npx wrangler d1 migrations apply graycode-cloud --local +npx wrangler d1 execute graycode-cloud --local --command "PRAGMA table_info(devices);" | grep -i version +``` + +Expected: the migration list shows `0024_graycode_capability_rename.sql` pending, apply succeeds, and the final command prints `graycode_version` with no `hawk_version` row. + +- [ ] **Step 3: Commit** + +```bash +git add apps/worker/migrations/0024_graycode_capability_rename.sql +git commit -m "feat: migrate device version and capability columns to graycode + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task B2: Rename the wire contract in the worker + +**Files:** +- Modify: `graycode-platform/apps/worker/contracts/v1.ts:8-16,23,55,124` +- Modify: `graycode-platform/apps/worker/src/domain/validation.ts:2,30,40` +- Modify: `graycode-platform/apps/worker/src/routes/auth.ts:32,48,56` +- Modify: `graycode-platform/apps/worker/src/routes/devices.ts:31,38,92` +- Modify: `graycode-platform/apps/worker/src/routes/sessions.ts:19` +- Modify: `graycode-platform/apps/worker/src/routes/organizations.ts:123` +- Modify: `graycode-platform/apps/worker/src/routes/enterprise.ts:596,748` +- Modify: `graycode-platform/apps/worker/src/auth/device-approve.ts:29,36,56,63` +- Test: `graycode-platform/apps/worker/test/{auth,devices,organizations,rate-limit,device-token,sessions,usage-sessions}.test.ts` + +**Interfaces:** +- Consumes: the column names from Task B1. +- Produces: `GRAYCODE_CAPABILITIES` (was `HAWK_CAPABILITIES`) with first element `'graycode'`; type `GraycodeCapability`; request field `graycodeVersion`. + +- [ ] **Step 1: Update the failing tests first** + +In `graycode-platform/apps/worker/test/auth.test.ts`, add a regression test that pins the CLI's actual request body: + +```ts +it('accepts the body graycode-cli actually sends', async () => { + const res = await app.request( + '/v1/auth/device/start', + { + method: 'POST', + headers: { 'content-type': 'application/json' }, + // Verbatim from graycode-cli internal/platform/cloud/client.go:105 + body: JSON.stringify({ + label: 'my-laptop', + platform: 'darwin', + graycodeVersion: '0.0.1', + }), + }, + env, + ) + expect(res.status).toBe(201) +}) +``` + +Match the surrounding tests' setup style for `app` and `env`; read the top of the file first. + +In `graycode-platform/apps/worker/test/usage-sessions.test.ts`, add: + +```ts +it('accepts the capability graycode-cli actually sends', async () => { + const res = await app.request( + '/v1/usage', + { + method: 'POST', + headers: { 'content-type': 'application/json', authorization: `Bearer ${deviceToken}` }, + // capability verbatim from graycode-cli cmd/exec.go:373 + body: JSON.stringify({ ...validUsageEvent, capability: 'graycode' }), + }, + env, + ) + expect(res.status).toBe(202) +}) +``` + +Reuse whatever fixture the neighbouring tests use in place of `validUsageEvent` and `deviceToken`. + +- [ ] **Step 2: Run the tests to verify they fail** + +```bash +cd graycode-platform/apps/worker && npx vitest run test/auth.test.ts test/usage-sessions.test.ts +``` + +Expected: FAIL. The device-start test gets 400 `Invalid device authorization request`. The usage test gets 400 `Invalid usage event`. + +- [ ] **Step 3: Rename the capability constant and type** + +In `graycode-platform/apps/worker/contracts/v1.ts`, replace lines 8-16: + +```ts +export const GRAYCODE_CAPABILITIES = [ + 'graycode', + 'swift', + 'shrike', + 'harrier', + 'merlin', + 'kestrel', +] as const +export type GraycodeCapability = (typeof GRAYCODE_CAPABILITIES)[number] +``` + +Update the two `capability: HawkCapability` fields at lines 23 and 55 to `GraycodeCapability`, and line 124 `hawkVersion: string` to `graycodeVersion: string`. Also reword the file's header comment (lines 1-7) from "Hawk Cloud" and "Hawk clients" to "Graycode Cloud" and "Graycode clients". + +- [ ] **Step 4: Update the validators** + +In `graycode-platform/apps/worker/src/domain/validation.ts`: line 2 imports `GRAYCODE_CAPABILITIES`, line 30 becomes `graycodeVersion: z.string().min(1).max(50),`, line 40 becomes `capability: z.enum(GRAYCODE_CAPABILITIES),`. + +In `graycode-platform/apps/worker/src/routes/auth.ts`: line 32 becomes `graycodeVersion: z.string().min(1).max(50),`; the INSERT at line 48 uses column `graycode_version`; the bound value at line 56 becomes `parsed.data.graycodeVersion`. + +In `graycode-platform/apps/worker/src/routes/sessions.ts`: line 19 becomes `capability: z.enum(GRAYCODE_CAPABILITIES),` with the matching import, replacing the inline literal array. + +- [ ] **Step 5: Update every SQL statement and its alias** + +Apply the same two mechanical substitutions in `src/routes/devices.ts:31,38,92`, `src/routes/organizations.ts:123`, `src/routes/enterprise.ts:596,748`, and `src/auth/device-approve.ts:29,36,56,63`: + +- column `hawk_version` becomes `graycode_version` +- SQL alias and TypeScript property `hawkVersion` becomes `graycodeVersion` + +- [ ] **Step 6: Update the remaining test fixtures** + +Replace `hawkVersion` with `graycodeVersion` at `test/auth.test.ts:116,125,317`, `test/organizations.test.ts:90`, `test/rate-limit.test.ts:153`, `test/devices.test.ts:77,106,118,135,153`. Replace `capability: 'hawk'` with `capability: 'graycode'` at `test/device-token.test.ts:166`, `test/sessions.test.ts:71,351`, `test/usage-sessions.test.ts:58,77,817`. + +```bash +cd graycode-platform/apps/worker +grep -rln 'hawkVersion' test/ | xargs sed -i '' 's/hawkVersion/graycodeVersion/g' +grep -rln "capability: 'hawk'" test/ | xargs sed -i '' "s/capability: 'hawk'/capability: 'graycode'/g" +grep -rn "hawkVersion\|capability: 'hawk'" test/ || echo "TEST FIXTURES CLEAN" +``` + +- [ ] **Step 7: Run the full worker suite** + +```bash +cd graycode-platform/apps/worker && npx tsc --noEmit && npx vitest run +``` + +Expected: typecheck clean, all tests PASS including the two added in Step 1. If `openapi-parity.test.ts` fails, that is expected until Task B4 updates the contract file; note it and continue. + +- [ ] **Step 8: Commit** + +```bash +git add apps/worker/contracts/v1.ts apps/worker/src apps/worker/test +git commit -m "feat!: rename the cloud wire contract from hawk to graycode + +Device login required hawkVersion while the CLI has always sent +graycodeVersion, and the capability enum rejected 'graycode', so +login returned 400 and every usage event was silently dropped by the +fail-open client. The wire now matches the product name. + +Requires migration 0024 to be applied before deploy. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task B3: Rename the capability enum in the BFF + +**Files:** +- Modify: `graycode-platform/apps/bff/src/routes/web-activity.ts:22,94` +- Modify: `graycode-platform/apps/bff/src/routes/web-dashboard.ts:15` + +**Interfaces:** +- Consumes: the capability values from Task B2. The BFF duplicates the enum as an inline literal rather than importing it. + +- [ ] **Step 1: Update both inline enums** + +In `graycode-platform/apps/bff/src/routes/web-activity.ts:94` and `web-dashboard.ts:15`, replace: + +```ts + tool: z.enum(['hawk', 'swift', 'shrike', 'harrier', 'merlin', 'kestrel']), +``` + +with: + +```ts + tool: z.enum(['graycode', 'swift', 'shrike', 'harrier', 'merlin', 'kestrel']), +``` + +Keep `.optional()` on the `web-activity.ts:94` occurrence. + +- [ ] **Step 2: Fix the user-facing achievement string** + +`web-activity.ts:22` reads `description: 'Completed your first hawk session'`. Change it to `'Completed your first graycode session'`. + +- [ ] **Step 3: Typecheck and test** + +```bash +cd graycode-platform/apps/bff && npx tsc --noEmit && npx vitest run +``` + +Expected: clean typecheck, tests PASS. + +- [ ] **Step 4: Commit** + +```bash +git add apps/bff/src/routes/web-activity.ts apps/bff/src/routes/web-dashboard.ts +git commit -m "feat: align the BFF capability enum with the graycode wire contract + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task B4: Update the OpenAPI contract and delete the stale third copy + +**Files:** +- Modify: `graycode-platform/api/graycode-cloud-openapi.yaml:3,1196,1230,1234,1322` +- Modify: `graycode-platform/apps/worker/contracts/openapi.yaml:1230,1234` (and the same title/enum lines) +- Delete: `graycode-platform/apps/worker/api/openapi.yaml` + +**Interfaces:** +- Consumes: the field names from Task B2. `test/openapi-parity.test.ts` reads `contracts/openapi.yaml` and must pass after this task. + +> **Three copies exist.** `api/graycode-cloud-openapi.yaml` and `apps/worker/contracts/openapi.yaml` are byte-identical. `apps/worker/api/openapi.yaml` is a stale 1371-line variant whose `servers.url` is the personal dev subdomain `https://graycode-cloud.lakshmanp230.workers.dev` (line 7). Nothing references it: no package script, no CI workflow, no turbo task. Deleting it removes both the drift and the personal subdomain in one step. + +- [ ] **Step 1: Confirm the stale copy is unreferenced before deleting** + +```bash +cd graycode-platform && grep -rn 'apps/worker/api/openapi\|api/openapi.yaml' --exclude-dir=node_modules --exclude-dir=.git . | grep -v '^./apps/worker/api/openapi.yaml' +``` + +Expected: no output. If anything references it, stop and repoint that reference at `apps/worker/contracts/openapi.yaml` instead of deleting. + +- [ ] **Step 2: Apply the renames to both live copies** + +```bash +cd graycode-platform +for f in api/graycode-cloud-openapi.yaml apps/worker/contracts/openapi.yaml; do + sed -i '' 's/hawkVersion/graycodeVersion/g' "$f" + sed -i '' 's/enum: \[hawk, swift/enum: [graycode, swift/' "$f" + sed -i '' 's/^ title: Hawk Cloud API/ title: Graycode Cloud API/' "$f" + sed -i '' 's/Control-plane API for Hawk and its related products/Control-plane API for Graycode and its related products/' "$f" + sed -i '' 's/Project-scoped hwc device token\./Project-scoped device token (`hwc_` prefix)./' "$f" +done +grep -n 'graycodeVersion\|enum: \[graycode\|title: Graycode Cloud' api/graycode-cloud-openapi.yaml +``` + +Expected: the rewritten lines print. The `hwc_` prefix itself stays; only its description changes. + +- [ ] **Step 3: Confirm the two live copies are still identical** + +```bash +cd graycode-platform && diff api/graycode-cloud-openapi.yaml apps/worker/contracts/openapi.yaml && echo "COPIES IN SYNC" +``` + +Expected: `COPIES IN SYNC`. + +- [ ] **Step 4: Delete the stale copy** + +```bash +cd graycode-platform && git rm apps/worker/api/openapi.yaml +``` + +- [ ] **Step 5: Run the parity test** + +```bash +cd graycode-platform/apps/worker && npx vitest run test/openapi-parity.test.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add api/graycode-cloud-openapi.yaml apps/worker/contracts/openapi.yaml +git commit -m "docs: rename the cloud contract to graycode and drop the stale copy + +apps/worker/api/openapi.yaml was an unreferenced 1371-line variant +still advertising a personal workers.dev subdomain as its server URL. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task B5: Align the CLI's cloud test fixture and document the endpoint + +**Files:** +- Modify: `graycode-cli/internal/platform/cloud/client_test.go:22` +- Modify: `graycode-cli/README.md` (the Portable Execution Graph section, around line 127) + +**Interfaces:** +- Consumes: the capability values from Task B2. No production CLI code changes; `client.go:105` and `exec.go:373` were already correct. + +> **A real gap this plan does not close.** `graycode cloud login` has no default endpoint: it requires `--endpoint` or `GRAYCODE_CLOUD_URL` (`cmd/cloud.go:41-46`). The worker's `wrangler.jsonc` declares no route or custom domain, so it is reachable only at its `workers.dev` address, and the contract's documented `servers.url` of `https://api.graycodeai.com` is the **BFF**, which requires a browser session cookie and returns 401 to a device token (`apps/bff/src/app.ts:54-58`). Choosing and provisioning a public hostname for the worker is an infrastructure decision outside this plan. Step 2 documents the requirement so users are not left guessing. + +- [ ] **Step 1: Fix the test fixture** + +In `graycode-cli/internal/platform/cloud/client_test.go:22`, change `Capability: "graycode"` — it is already correct and matches the renamed enum. Verify rather than edit: + +```bash +cd graycode-cli && grep -n 'Capability:' internal/platform/cloud/client_test.go +``` + +Expected: `Capability: "graycode"`. No edit needed. If it reads `"hawk"`, change it to `"graycode"`. + +- [ ] **Step 2: Document that the cloud endpoint must be supplied** + +In `graycode-cli/README.md`, directly under the `graycode cloud graph sync` code block, add: + +```markdown +Cloud commands require an endpoint. There is no default: pass `--endpoint` or +set `GRAYCODE_CLOUD_URL` to your Graycode Cloud worker URL before running +`graycode cloud login`. `https://api.graycodeai.com` is the browser BFF and +will reject a device token. +``` + +- [ ] **Step 3: Verify and commit** + +```bash +cd graycode-cli && go test ./internal/platform/cloud/ -v +git add internal/platform/cloud/client_test.go README.md +git commit -m "docs: state that graycode cloud requires an explicit endpoint + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +- [ ] **Step 4: Deploy in the correct order** + +After the graycode-platform PRs merge: + +```bash +cd graycode-platform/apps/worker +npx wrangler d1 migrations apply graycode-cloud --remote # 0024 FIRST +cd ../.. && pnpm deploy:worker +pnpm deploy:bff +``` + +Expected: migration reports one statement batch applied, then both workers deploy. Deploying the worker before the migration breaks every device read. + +--- + +# Workstream C — Router Boundary Truth + +**Problem.** This workstream fixes documentation and two defective guards. **There is no import violation to repair: all nine boundary guards pass today.** + +What is actually wrong: + +1. **The documented boundary contradicts the enforced one.** `graycode-router/AGENTS.md:570-572` says graycode-cli must not assemble anything below `engine`. In reality all three enforcement layers deliberately allow four packages — `engine`, `llm`, `graph`, `tools` — and eight non-test files depend on that allowance (`internal/config/catalog_api.go:9`, `internal/engine/client_interface.go:7`, `internal/engine/compact_provider_native.go:8`, `internal/engine/execution_graph_observations.go:16`, `internal/provider/gateway/engine_client.go:16`, `internal/provider/gateway/gateway.go:15`, `internal/session/session.go:23`, `internal/types/client.go:6`). Six symbols they use have no `engine` equivalent at all: `ChatOptions`, `ContinuationConfig`, `StreamResult`/`NewStreamResult`, `ResponseFormat`, `ImageURLPart`, `InputAudioPart`, plus the `ModelClass*` constants and `GatewayRegionOption`. +2. **A guard that can never fail.** `graycode-cli/scripts/check-support-repo-coupling.sh:30-32` builds its regex from the peer list. With `graycode-router` the only engine in `ecosystem.yaml`, the peer list is empty and the pattern degenerates to `github\.com/GrayCodeAI/()(/|")`. Verified with `bash -x`. +3. **A guard missing from pre-push.** `lefthook.yml:115-125` runs three boundary scripts but omits `check-graycode-router-engine-boundary.sh` (verified: zero occurrences in the file). It runs in CI, so violations are caught late instead of before push. +4. **A dead exception.** Both Go AST tests carry a `internal/provider/gateway` → `credentials` exception (`internal/testaudit/audit_test.go:223-226`, `internal/testaudit/package_boundaries_test.go:61-65`). Zero non-test files import `graycode-router/credentials`. + +**Approach.** Document the four-package contract as the real boundary, make the vacuous guard honest, and add the missing hook. Do not attempt to collapse the CLI onto `engine`-only; six required symbols are unexported from the facade and widening `engine` is a graycode-router API change that belongs in its own plan. + +--- + +### Task C1: Make the documented boundary match the enforced one + +**Files:** +- Modify: `graycode-router/AGENTS.md:551,570-572` +- Modify: `graycode-router/README.md:48-53` (the Ecosystem Boundaries section) +- Modify: `graycode-cli/scripts/check-graycode-router-engine-boundary.sh:18-19` (comment only) + +**Interfaces:** +- Produces: one written contract naming exactly four allowed packages, cited by both repos. + +- [ ] **Step 1: State the real contract in graycode-router** + +In `graycode-router/README.md`, replace the Ecosystem Boundaries bullets at lines 48-53: + +```markdown +graycode-router is a Graycode support engine. Keep the dependency edge one-way. + +Hosts may import exactly four packages: + +| Package | Carries | +|---|---| +| `engine` | the stable host-facing facade | +| `llm` | host-facing DTOs and the `Provider` port that `engine` re-exports as aliases | +| `graph` | the portable execution-graph vocabulary | +| `tools` | tool-call and tool-result contracts | + +Everything else is engine-internal: `client`, `catalog`, `config`, +`credentials`, `router`, `runtime`, and their subpackages are not shared +contracts. Enforced by `graycode-cli/scripts/check-graycode-router-engine-boundary.sh` +and two Go AST tests in `graycode-cli/internal/testaudit/`. + +- do not import `graycode-cli/internal/*` +- do not import the removed legacy path `graycode/shared/types` +- do not import other engines (`harrier`, `shrike`, `swift`, `kestrel`, + `merlin`) — engines are peers, not dependencies +``` + +- [ ] **Step 2: Correct the AGENTS.md pitfall** + +In `graycode-router/AGENTS.md`, replace the first Common Pitfalls bullet (lines 570-572): + +```markdown +- `engine`, `llm`, `graph` and `tools` are the host contract surface. Graycode + must not assemble `client`, `catalog`, `config`, `credentials`, `router` or + `runtime`. Six symbols Graycode needs (`ChatOptions`, `ContinuationConfig`, + `StreamResult`, `ResponseFormat`, `ImageURLPart`, `InputAudioPart`) live in + `llm` with no `engine` alias; widening the facade to cover them is a + deliberate API change, not an incidental one. +``` + +- [ ] **Step 3: Correct the misleading guard comment** + +`graycode-cli/scripts/check-graycode-router-engine-boundary.sh:18-19` currently claims Graycode uses "the full vendored GraycodeRouter API surface", which overstates a four-package allowance. Replace both comment lines: + +```bash +# Host contract surface is exactly four packages: engine (facade), llm (DTOs +# and the Provider port), graph (portable graph vocabulary), tools (tool-call +# contracts). See graycode-router/README.md "Ecosystem Boundaries". +``` + +- [ ] **Step 4: Verify the guards still pass** + +```bash +cd graycode-cli && bash ./scripts/check-graycode-router-engine-boundary.sh && echo "EXIT=$?" +``` + +Expected: `graycode-router engine boundary passed (zero lower-level production imports)` then `EXIT=0`. + +- [ ] **Step 5: Commit (two repos, two commits)** + +```bash +cd graycode-router +git add README.md AGENTS.md +git commit -m "docs: document the four-package host contract surface + +AGENTS.md claimed engine-only while all three enforcement layers +deliberately allow engine, llm, graph and tools, and eight production +files depend on that allowance. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" + +cd ../graycode-cli +git add scripts/check-graycode-router-engine-boundary.sh +git commit -m "docs: correct the router boundary guard comment + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task C2: Repair the two defective guards + +**Files:** +- Modify: `graycode-cli/scripts/check-support-repo-coupling.sh:29-33` +- Modify: `graycode-cli/lefthook.yml:118-125` +- Modify: `graycode-cli/internal/testaudit/audit_test.go:223-226` +- Modify: `graycode-cli/internal/testaudit/package_boundaries_test.go:61-65` + +**Interfaces:** +- Produces: a peer guard that reports honestly when it has nothing to check, an engine-boundary check on pre-push, and both AST tests with the dead exception removed. + +- [ ] **Step 1: Make the vacuous guard honest** + +In `graycode-cli/scripts/check-support-repo-coupling.sh`, guard the empty-peer case before building the pattern. Insert immediately after the `pattern=` assignment at line 30: + +```bash + if [[ ${#peers[@]} -eq 0 ]]; then + # No sibling engines to check against. Building a regex from an empty + # peer list produced 'github\.com/GrayCodeAI/()(/|")', which matches + # nothing meaningful and made this guard silently unfailable. + echo "peer guard: ${repo} has no sibling engines to check" + continue + fi +``` + +Confirm `peers` is the array name and `continue` is inside the per-repo loop; read lines 20-40 first and adapt the variable names to what is actually there. + +- [ ] **Step 2: Verify the guard now reports rather than pretending** + +```bash +cd graycode-cli && bash ./scripts/check-support-repo-coupling.sh; echo "EXIT=$?" +``` + +Expected: `peer guard: graycode-router has no sibling engines to check` followed by the existing pass line, `EXIT=0`. + +- [ ] **Step 3: Add the missing pre-push hook** + +In `graycode-cli/lefthook.yml`, add a fourth boundary command alongside the existing three (after the `boundary-graycode-router-client` block ending at line 119): + +```yaml + boundary-graycode-router-engine: + run: bash scripts/check-graycode-router-engine-boundary.sh +``` + +- [ ] **Step 4: Verify lefthook parses and the hook is registered** + +```bash +cd graycode-cli && python3 -c "import yaml; d=yaml.safe_load(open('lefthook.yml')); print(sorted(d['pre-push']['commands']))" +``` + +Expected: the list includes `boundary-graycode-router-engine`. + +- [ ] **Step 5: Remove the dead credentials exception** + +Delete the exception block at `graycode-cli/internal/testaudit/audit_test.go:223-226`: + +```go + if strings.HasPrefix(rel, "internal/provider/gateway/") && path == graycodeRouterModule+"/credentials" { + continue + } +``` + +And at `graycode-cli/internal/testaudit/package_boundaries_test.go:61-65`, the equivalent block keyed on `filepath.ToSlash(filepath.Dir(relFile)) == "internal/provider/gateway"`. + +Verify nothing depended on them first: + +```bash +cd graycode-cli && grep -RInE --include='*.go' --exclude='*_test.go' 'graycode-router/credentials' . ; echo "exit=$? (1 means clean)" +``` + +Expected: no output, `exit=1`. + +- [ ] **Step 6: Run both AST tests and the whole guard set** + +```bash +cd graycode-cli && go test ./internal/testaudit/ -run 'TestNoDirectLowerGraycodeRouterImports|TestPackageDependencyGraph' -count=1 -v && make boundaries +``` + +Expected: both tests PASS, all guards pass. + +- [ ] **Step 7: Commit** + +```bash +git add scripts/check-support-repo-coupling.sh lefthook.yml internal/testaudit/audit_test.go internal/testaudit/package_boundaries_test.go +git commit -m "fix: repair two boundary guards that could not fail + +check-support-repo-coupling.sh built its regex from an empty peer list, +degenerating to a pattern matching nothing. The engine-boundary script +ran in CI but not on pre-push. Both AST tests carried a +gateway->credentials exception that no production file uses. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +# Workstream D — Naming Sweep + +**Scope (approved):** documentation, user-visible strings, and code comments. Roughly 230 sites. `graycode-cli` is already clean: it contains **zero** occurrences of `hawk` outside generated GitNexus blocks. Marketing copy under `graycode-platform/apps/web` is out of scope; it is dated content and product-line branding. + +Every load-bearing identifier listed in Global Constraints stays untouched. When in doubt, a name that appears in a wire payload, a database column, an environment variable, a filesystem path, a Cloudflare binding, a Go module path, or a URL route is load-bearing. + +**Two bonus defects folded in here:** +- A prior substring mass-rename corrupted prose in 15 places: `trace`→`swift` produced "Step-by-step execution swift", `inspect`→`merlin` produced "Merlin configured MCP servers", `tok`→`shrike` produced "ref-shrike". +- `graycode-skills` advertises "12,171+ skills" and "31 categories". The real counts are **12,167 skills** and **27 categories** (verified by `find … -name SKILL.md | wc -l` and `ls categories | wc -l`). + +--- + +### Task D1: graycode-skills — rebrand from starling and correct the counts + +**Files:** +- Modify: `README.md:1,3,7,16,19,22,25,99-102`, `CONTRIBUTING.md:1,3,82,175`, `AGENTS.md:2,7,9,44-48`, `SECURITY.md:1`, `CHANGELOG.md:3`, `api/openapi.yaml:3,5,9,13,15,17,38-39`, `docs/architecture.md:3,5,16,18,25,45`, `pyproject.toml:8`, `tools/init_skill.py:59`, `tools/sign_manifest.py:141`, `scripts/check-consumer-boundaries.sh:10` + +**Interfaces:** +- Produces: no code behavior change. `pyproject.toml` package `name = "starling"` at line 6 is **not** changed here; renaming a published package name is a release decision. + +- [ ] **Step 1: Correct the counts and the CLI name in the README** + +In `graycode-skills/README.md`: +- Line 1: `# hawk Community Skills` → `# Graycode Community Skills` +- Line 3: `[hawk](https://github.com/GrayCodeAI/hawk)` → `[Graycode](https://github.com/GrayCodeAI/graycode-cli)`, and `12,171+` → `12,167`, and `31 categories` → `27 categories` +- Line 7: `that hawk loads` → `that Graycode loads` +- Lines 16, 19, 25: `hawk skills list` → `graycode skills list`, `hawk skills search api-testing` → `graycode skills search api-testing`, `the hawk REPL` → `the graycode REPL` +- Line 22: `hawk skills install python-pandas` → `graycode skills install GrayCodeAI/graycode-skills python-pandas` + +> The install syntax genuinely differs: `graycode skills install` takes ` [skill-name]` (`graycode-cli/cmd/skills_cmd.go:74`), not a bare skill name. Documenting the bare form would keep the command broken even after Workstream A. + +- [ ] **Step 2: Fix the Ecosystem Boundaries block** + +`README.md:99-102` and `AGENTS.md:44-46` list **pre-rename engine names** that no longer exist: `yaad`, `tok`, `trace`, `sight`, `inspect`. Replace with the live names: + +```markdown +- `graycode-skills` extends Graycode through public skill and plugin surfaces. +- Do not reference support engine repos (`graycode-router`, `harrier`, `shrike`, + `swift`, `kestrel`, `merlin`) as direct dependencies. +- Do not reference `graycode-cli/internal/*` or the removed legacy path + `graycode/shared/types`. +- Skills should assume Graycode is the product boundary. +``` + +- [ ] **Step 3: Update the boundary guard's alternation to match** + +`graycode-skills/scripts/check-consumer-boundaries.sh` still forbids the pre-rename engine names. The pattern is inline inside the `grep -RInE` call at lines 7-12, not a variable. Replace the pattern argument on line 10: + +```bash + 'github\.com/GrayCodeAI/(graycode-router|harrier|shrike|swift|kestrel|merlin)(/|")|github\.com/GrayCodeAI/graycode-cli/(internal/|shared/types)' \ +``` + +Also update the two failure messages at lines 16 and 19 so they name Graycode rather than Hawk and graycode-skills rather than starling. + +Verify it still runs clean: + +```bash +cd graycode-skills && bash scripts/check-consumer-boundaries.sh; echo "EXIT=$?" +``` + +Expected: `EXIT=0`. This guard scans `README.md docs api tests tools .claude-plugin .codex-plugin .cursor-plugin`, so it will fail if Step 2's replacement text accidentally reintroduces a forbidden module path. + +- [ ] **Step 4: Sweep the remaining prose files** + +```bash +cd graycode-skills +for f in CONTRIBUTING.md AGENTS.md SECURITY.md CHANGELOG.md api/openapi.yaml docs/architecture.md pyproject.toml tools/init_skill.py tools/sign_manifest.py; do + sed -i '' -e 's/\bstarling\b/graycode-skills/g' -e 's/\bStarling\b/Graycode Skills/g' \ + -e 's/\bhawk-eco\b/graycode-eco/g' -e 's/\bHawk\b/Graycode/g' -e 's/\bhawk\b/graycode/g' "$f" +done +sed -i '' 's/12,171+/12,167/g; s/31 categories/27 categories/g' CONTRIBUTING.md +sed -i '' 's/^name = "graycode-skills"/name = "starling"/' pyproject.toml # restore the package name +grep -rn 'GrayCodeAI/graycode-skills' pyproject.toml || true +``` + +- [ ] **Step 5: Repair what the blunt sweep broke** + +The `sed` above also rewrites the GitNexus block in `AGENTS.md` and any load-bearing identifier. Restore them: + +```bash +cd graycode-skills +git diff --stat +git diff | grep -nE '^\+.*(AGENT_ENUM|progressive-disclosure|/graycode:|sync_marketplace)' || echo "no load-bearing identifiers touched" +``` + +Inspect `git diff` in full. Revert any hunk that changes a value inside a `` block, the `/hawk:` invoke prefix, the `AGENT_ENUM` values, or the `hawk-progressive-disclosure` marker. Those files (`tools/sync_marketplace.py`, `tools/validate_skill.py`, `tools/migrate_oversized_skills.py`) are not in the Step 4 loop, but verify nothing else drifted. + +- [ ] **Step 6: Verify the corpus still validates** + +```bash +cd graycode-skills && python -m pytest -q && python tools/validate_skill.py --all --warning-budget tools/validation_warning_budget.json && ruff check . +``` + +Expected: tests PASS, zero validation warnings, ruff clean. + +- [ ] **Step 7: Commit** + +```bash +git add -A +git commit -m "docs: rebrand from starling/hawk to graycode-skills/graycode + +Also corrects the advertised counts (12,167 skills across 27 +categories, not 12,171+ across 31), documents the real +'graycode skills install ' syntax, and replaces the +pre-rename engine names in the boundary docs and guard. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task D2: graycode-router — docs, user-facing strings, and comments + +**Files:** +- Modify: `README.md:33,39,42,48,50,52,53,177,305,309`, `AGENTS.md:18,52,58,144`, `CONTRIBUTING.md:4`, `SECURITY.md:10`, `docs/ARCHITECTURE.md:17`, `docs/architecture/HOST-ENGINE-BOUNDARY.md` (21 lines), `docs/guides/DYNAMIC-MODEL-DISCOVERY.md` (24 lines), `docs/guides/CREDENTIAL-SETUP-FLOW.md:1,3,38,53,72`, `docs/design/GRAYCODE-ROUTER-ENTERPRISE.md:61,78` +- Modify (user-facing strings): `catalog/v1.go:633`, `setup/status.go:135`, `runtime/preflight.go:48` +- Modify (comments): `llm/types.go`, `llm/provider.go` and ~140 further comment lines +- Modify: `scripts/test-config-flow.sh:44` +- Modify: `scripts/check-ecosystem-boundaries.sh:10` + +**Interfaces:** +- Produces: no behavior change. Three user-visible error strings change wording only. + +- [ ] **Step 1: Fix the three user-facing strings first** + +These are the only sweep items a user can actually see in the terminal. + +- `graycode-router/catalog/v1.go:633`: `run: hawk models refresh` → `run: graycode models refresh` +- `graycode-router/setup/status.go:135`: `hawk refreshes automatically; use \`hawk models refresh\`` → `graycode refreshes automatically; use \`graycode models refresh\`` +- `graycode-router/runtime/preflight.go:48`: `hawk will discover on /config` → `graycode will discover on /config` + +- [ ] **Step 2: Write a test that pins them** + +Create `graycode-router/setup/naming_test.go`: + +```go +package setup + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestNoLegacyHostNameInUserFacingStrings guards the three call sites that +// print a command for the user to run. They named the old product. +func TestNoLegacyHostNameInUserFacingStrings(t *testing.T) { + files := []string{ + filepath.Join("..", "catalog", "v1.go"), + filepath.Join("..", "setup", "status.go"), + filepath.Join("..", "runtime", "preflight.go"), + } + for _, f := range files { + data, err := os.ReadFile(f) // #nosec G304 -- fixed test fixture paths + if err != nil { + t.Fatalf("read %s: %v", f, err) + } + for i, line := range strings.Split(string(data), "\n") { + if !strings.Contains(line, `"`) { + continue + } + if strings.Contains(line, "hawk models refresh") || strings.Contains(line, "hawk will discover") || strings.Contains(line, "hawk refreshes") { + t.Errorf("%s:%d prints the legacy host name to the user: %s", f, i+1, strings.TrimSpace(line)) + } + } + } +} +``` + +- [ ] **Step 3: Run it** + +```bash +cd graycode-router && go test ./setup/ -run TestNoLegacyHostNameInUserFacingStrings -v +``` + +Expected: PASS after Step 1. Revert one string temporarily to confirm the test can fail, then restore it. + +- [ ] **Step 4: Sweep the documentation** + +```bash +cd graycode-router +for f in README.md AGENTS.md CONTRIBUTING.md SECURITY.md docs/ARCHITECTURE.md \ + docs/architecture/HOST-ENGINE-BOUNDARY.md docs/guides/DYNAMIC-MODEL-DISCOVERY.md \ + docs/guides/CREDENTIAL-SETUP-FLOW.md docs/design/GRAYCODE-ROUTER-ENTERPRISE.md; do + sed -i '' -e 's|GrayCodeAI/hawk|GrayCodeAI/graycode-cli|g' -e 's/\bhawk-eco\b/graycode-eco/g' \ + -e 's/\bHawk\b/Graycode/g' -e 's/\bhawk\b/graycode/g' "$f" +done +grep -rn '\bhawk\b\|\bHawk\b' README.md AGENTS.md docs/ || echo "DOCS CLEAN" +``` + +- [ ] **Step 5: Fix the three doc facts the sweep cannot fix** + +- `README.md:50` says DTOs live in `eagle/llm`. The `eagle` module was removed and vendored. Change to: ``host-facing DTOs and the `Provider` port live in `llm/`; `engine/` re-exports them as aliases``. +- `SECURITY.md:10` and `CONTRIBUTING.md:4` link `VERSIONING.md` at the repo root of a repo that no longer exists. Point both at `https://github.com/GrayCodeAI/graycode-cli/blob/main/docs/versioning.md`. +- `docs/guides/CREDENTIAL-SETUP-FLOW.md:53` cites `hawk/cmd/chat_config_xiaomi.go`. Verify the current filename before writing a replacement: + +```bash +ls ../graycode-cli/cmd/ | grep -i 'chat_config' +``` + +Use whichever file actually exists; if none matches, delete the file reference rather than inventing one. + +- [ ] **Step 6: Remove the hard-coded personal path** + +`graycode-router/scripts/test-config-flow.sh:44` contains an absolute path into a previous working directory. Replace: + +```bash +cd "$(dirname "$0")/.." +``` + +- [ ] **Step 7: Extend the ecosystem guard to the current repo name** + +`graycode-router/scripts/check-ecosystem-boundaries.sh:10` forbids only `github.com/GrayCodeAI/hawk`, a module that no longer exists, while permitting the real host module. Widen the pattern, keeping the variable name `FORBIDDEN_HAWK` because it is read at lines 16 and 20: + +```bash +FORBIDDEN_HAWK='github\.com/GrayCodeAI/(hawk|graycode-cli)(/|")' +``` + +The comment above it at lines 7-9 also claims shared vocabulary "belongs in eagle", a module that was removed and vendored into `graycode-cli/internal/contracts`. Rewrite it: + +```bash +# GraycodeRouter is host-neutral: it must not depend on any Graycode package. +# Shared ecosystem vocabulary lives in graycode-cli/internal/contracts, which +# hosts vendor rather than import from here. +``` + +Verify: + +```bash +cd graycode-router && bash scripts/check-ecosystem-boundaries.sh; echo "EXIT=$?" +``` + +Expected: `EXIT=0`. + +- [ ] **Step 8: Sweep the Go comments** + +```bash +cd graycode-router +grep -rln '\bhawk\b\|\bHawk\b' --include='*.go' . | while read -r f; do + sed -i '' -e 's|// \(.*\)\bHawk\b|// \1Graycode|g' -e 's|// \(.*\)\bhawk\b|// \1graycode|g' "$f" +done +git diff --stat +``` + +Then inspect the diff and revert every hunk touching a **string literal** rather than a comment, and every load-bearing identifier: `HAWK_CONFIG_DIR` (`config/provider_env.go:402`, `config/category.go:122`, `engine/engine.go`), the `~/.hawk` paths (`credentials/migrate.go:14,88`), and `hawk_build` / `hawk_build_concise` (`tools/versioning.go:114,115`). + +```bash +git diff | grep -nE '^\+.*(HAWK_CONFIG_DIR|\.hawk|hawk_build)' && echo "REVERT THESE HUNKS" || echo "no load-bearing identifiers touched" +``` + +- [ ] **Step 9: Build, test, format** + +```bash +cd graycode-router && gofumpt -l . && go build ./... && go test ./... && go vet ./... +``` + +Expected: `gofumpt -l` prints nothing, build and vet clean, all tests PASS. + +- [ ] **Step 10: Commit** + +```bash +git add -A +git commit -m "docs: rename the host from hawk to graycode across docs and comments + +Also fixes three user-facing strings that told users to run +'hawk models refresh', corrects the removed eagle/llm reference, +repoints dead VERSIONING.md links, removes a hard-coded personal path +from test-config-flow.sh, and extends the ecosystem guard to the +current host module name. + +Keeps HAWK_CONFIG_DIR, ~/.hawk migration paths and hawk_build tool +namespaces: those are compatibility values, not prose. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task D3: graycode-platform — docs and the rename-corruption typos + +**Files:** +- Modify: `README.md:16,20,29,40,56,94,103`, `ARCHITECTURE.md:5,13,254,314`, `AGENTS.md:34,38,70`, `CLAUDE.md:31`, `CONTRIBUTING.md:4`, `SECURITY.md:10`, `CHANGELOG.md:3`, `docs/architecture.md:6`, `apps/worker/README.md:1,70,82`, `apps/bff/README.md:7`, `apps/worker/docs/{ENCRYPTION-KEY-ROTATION,DELIVERY-CONTEXT-FLOW,ARCHITECTURE-IMPLEMENTATION}.md`, `apps/worker/migrations/0003_cli_device_authorization.sql:1` (comment only), `apps/worker/src/app.ts:57`, `scripts/deploy.sh:24,26` + +**Interfaces:** +- Consumes: nothing. Runs independently of Workstream B, though B should land first so the docs describe the shipped contract. + +- [ ] **Step 1: Fix the three rename-corruption typos** + +A prior `trace`→`swift` substring rename corrupted generated GitNexus tables. In `AGENTS.md:70` and `CLAUDE.md:31`: + +```markdown +| `gitnexus://repo/graycode-platform/process/{name}` | Step-by-step execution trace | +``` + +Check the same corruption in the other three repos: + +```bash +cd /Users/lakshmanpatel/Desktop/OSS2026/graycode-eco +grep -rn 'execution swift' --include='*.md' --exclude-dir=node_modules --exclude-dir=.git . +``` + +Fix every hit to `execution trace`. + +- [ ] **Step 2: Fix the stale README warning** + +`graycode-platform/README.md:56` warns about `lakshmanp230.workers.dev` and names `apps/web/public/_headers`, `apps/web/dist/_headers` and `api/graycode-cloud-openapi.yaml`. Those three files are already clean; the only remaining occurrence was `apps/worker/api/openapi.yaml:7`, deleted in Task B4. Replace the whole warning block with an accurate one: + +```markdown +> ⚠️ The Graycode Cloud worker has no route or custom domain in +> `apps/worker/wrangler.jsonc`, so it is reachable only at its generated +> `workers.dev` address. CLI device traffic needs a stable hostname before +> launch; `api.graycodeai.com` is the browser BFF and rejects device tokens. +``` + +- [ ] **Step 3: Fix the architecture diagram's dead host** + +`ARCHITECTURE.md:314` renders a node labelled `graycode-api.workers.dev`, a host that does not exist. Replace with `api.graycodeai.com`. + +- [ ] **Step 4: Sweep the prose** + +```bash +cd graycode-platform +for f in README.md ARCHITECTURE.md AGENTS.md CONTRIBUTING.md SECURITY.md CHANGELOG.md \ + docs/architecture.md apps/worker/README.md apps/bff/README.md \ + apps/worker/docs/ENCRYPTION-KEY-ROTATION.md apps/worker/docs/DELIVERY-CONTEXT-FLOW.md \ + apps/worker/docs/ARCHITECTURE-IMPLEMENTATION.md; do + sed -i '' -e 's/Hawk Cloud/Graycode Cloud/g' -e 's|GrayCodeAI/hawk|GrayCodeAI/graycode-cli|g' \ + -e 's/\bhawk-eco\b/graycode-eco/g' -e 's/\bHawk\b/Graycode/g' -e 's/\bhawk\b/graycode/g' "$f" +done +``` + +- [ ] **Step 5: Restore the identifiers the sweep clobbered** + +```bash +cd graycode-platform +git diff | grep -nE '^\+.*(GraycodeCloudService|GRAYCODE_CLOUD"|graycode\.window_days|graycode\.sessions|graycode\.tokens)' && echo "REVERT THESE" || echo "clean" +``` + +`HawkCloudService` (RPC entrypoint), `HAWK_CLOUD` (service binding) and the `hawk.*` OTel metric names must survive. Revert any hunk that renamed them. In `apps/worker/README.md:70` the sentence should read: "The BFF calls Graycode Cloud through the named `HawkCloudService` RPC entrypoint." + +- [ ] **Step 6: Fix the login command name** + +`apps/worker/README.md:82` and `apps/worker/docs/ENCRYPTION-KEY-ROTATION.md:34` reference `hawk login`. The real command is `graycode cloud login` (`graycode-cli/cmd/cloud.go:35`). The Step 4 sweep produces `graycode login`, which is wrong. Correct both to `graycode cloud login`. + +```bash +cd graycode-platform && grep -rn 'graycode login' apps/ docs/ *.md +``` + +Expected after fixing: no output. + +- [ ] **Step 7: Fix the two remaining strings** + +- `apps/worker/src/app.ts:57`: `'Hawk Cloud request failed'` → `'Graycode Cloud request failed'` +- `scripts/deploy.sh:24,26`: `Deploying Hawk Cloud Worker` and `Hawk Cloud Worker deployed` → `Graycode Cloud Worker` +- `apps/worker/migrations/0003_cli_device_authorization.sql:1`: the comment mentioning `hawk login` → `graycode cloud login`. **Comment only. The filename and the column stay** — the column is renamed by migration `0024`, never by editing an applied migration. + +- [ ] **Step 8: Typecheck, test, format** + +```bash +cd graycode-platform && pnpm --filter @graycode/worker check && pnpm --filter @graycode/bff check +``` + +Expected: prettier clean, typecheck clean, all tests PASS. + +- [ ] **Step 9: Commit** + +```bash +git add -A +git commit -m "docs: rename Hawk Cloud to Graycode Cloud across platform docs + +Also corrects 'graycode login' to the real 'graycode cloud login' +command, replaces a stale workers.dev warning that named three +already-clean files, fixes a dead graycode-api.workers.dev node in the +architecture diagram, and repairs 'execution swift' typos left by an +earlier substring rename. + +Keeps HAWK_CLOUD, HawkCloudService and the hawk.* OTel metric names. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +### Task D4: graycode-cli — refresh the ecosystem inventory and drop starling fixtures + +**Files:** +- Modify: `graycode-cli/README.md` (the Ecosystem section, around lines 442-490) +- Modify: `graycode-cli/internal/plugin/registry_test.go:20,21,167,192,250,276,285` +- Modify: `graycode-cli/internal/plugin/skillslock_test.go:20` + +**Interfaces:** +- Consumes: the four-package contract from Task C1. + +> `graycode-cli` has zero `hawk` occurrences. The only stale names are `starling` in test fixtures and an ecosystem table that lists repos not present in `ecosystem.yaml`. + +- [ ] **Step 1: Replace the starling fixtures** + +```bash +cd graycode-cli +sed -i '' 's|GrayCodeAI/starling|GrayCodeAI/graycode-skills|g' internal/plugin/registry_test.go internal/plugin/skillslock_test.go +grep -rn 'starling' internal/ cmd/ || echo "CLI CLEAN" +``` + +- [ ] **Step 2: Correct the ecosystem section** + +`README.md:442-490` names `owl`, `falcon` and other repos as ecosystem members, but `ecosystem.yaml` lists exactly four: `graycode-cli`, `graycode-router`, `graycode-skills`, `graycode-platform`. Make the prose match the manifest, and extend the component table at the end of the section: + +```markdown +| Component | Repository | Purpose | +|---|---|---| +| **graycode** | This repo | AI coding agent | +| **graycode-router** | [GrayCodeAI/graycode-router](https://github.com/GrayCodeAI/graycode-router) | LLM provider runtime | +| **graycode-skills** | [GrayCodeAI/graycode-skills](https://github.com/GrayCodeAI/graycode-skills) | Community skill registry | +| **graycode-platform** | [GrayCodeAI/graycode-platform](https://github.com/GrayCodeAI/graycode-platform) | Web, BFF, and Graycode Cloud | + +`ecosystem.yaml` is the canonical inventory; tooling reads it rather than +carrying its own list. Support engines mounted through Go module +dependencies (`harrier`, `shrike`, `swift`, `kestrel`, `merlin`, `falcon`) +live in their own repositories and are not part of this workspace. +``` + +- [ ] **Step 3: Verify the manifest and the prose agree** + +```bash +cd graycode-cli && ./scripts/ecosystem-manifest.sh list && go test ./internal/plugin/ ./internal/testaudit/ -count=1 +``` + +Expected: the manifest lists exactly the four repos named in the table; tests PASS. + +- [ ] **Step 4: Commit** + +```bash +git add README.md internal/plugin/registry_test.go internal/plugin/skillslock_test.go +git commit -m "docs: align the ecosystem section with ecosystem.yaml + +Also replaces GrayCodeAI/starling test fixtures with the repo's +current name. + +Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" +``` + +--- + +# Execution Order + +The workstreams are independent, but two ordering constraints are real: + +1. **A1 → A2 → A3.** The CLI cannot be verified until the release URL is live. +2. **B1 → B2/B3/B4 → deploy.** The migration must be applied before the worker deploys. + +Recommended sequence, one PR per repo per workstream: + +| Order | Workstream | Repos | Blocking? | +|---|---|---|---| +| 1 | A (registry) | graycode-skills, then graycode-cli | A2 must merge and run before A3 verifies | +| 2 | B (wire contract) | graycode-platform, graycode-cli | migration before deploy | +| 3 | C (boundary truth) | graycode-router, graycode-cli | none | +| 4 | D (naming) | all four | run last so it sweeps the text A/B/C introduce | + +--- + +# Self-Review + +**Coverage.** Every finding from the scouting pass maps to a task: registry publishing (A2), registry shape (A1), registry consumption (A3), install discovery (A4), device-login field drift (B1-B2), usage capability drift (B1-B2), BFF enum (B3), triplicate OpenAPI plus personal subdomain (B4), missing cloud endpoint documented as a known gap (B5), documented-vs-enforced boundary (C1), vacuous peer guard (C2), missing pre-push hook (C2), dead credentials exception (C2), legacy naming (D1-D4), wrong skill and category counts (D1), rename-corruption typos (D3), hard-coded personal path (D2). + +**Deliberately out of scope, and why.** +- Widening the `engine` facade to re-export `ChatOptions`, `StreamResult`, `ResponseFormat`, `ContinuationConfig`, `ImageURLPart` and `InputAudioPart` so graycode-cli could be `engine`-only. That is a graycode-router public API change and deserves its own plan. +- Provisioning a public hostname for the Graycode Cloud worker. Infrastructure decision, flagged in B5. +- Renaming the `starling` Python package name in `pyproject.toml:6`. A published-name change is a release decision. +- Marketing copy under `apps/web`, including the blog post claiming 12,147 skills across 21 categories. Dated content; correcting it is an editorial call. +- `graycode-cli`'s own `docs/` tree, which mentions competitors and legacy names inside dated design documents. + +**Known risk.** Tasks D1, D2 and D3 use `sed` sweeps followed by a manual diff review. The review step is not optional: each sweep is capable of rewriting a load-bearing identifier, and each of those tasks carries an explicit revert-check step naming the identifiers at risk. If a sweep's diff exceeds what a reviewer can read carefully, split it per file rather than trusting the pattern. From 752a8ee007014fd538e8d51297ea3ded534fb09e Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Mon, 7 Sep 2026 16:15:03 +0530 Subject: [PATCH 2/2] docs: fix markdownlint violations in the ecosystem integrity plan - Promote the four workstream headings and Execution Order / Self-Review from H1 to H2 so the document has a single top-level title (MD025) and heading levels increment by one (MD001). - Convert hard tabs to spaces in every code block (MD010). Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k --- .../2026-09-05-graycode-eco-integrity.md | 388 +++++++++--------- 1 file changed, 194 insertions(+), 194 deletions(-) diff --git a/docs/plans/2026-09-05-graycode-eco-integrity.md b/docs/plans/2026-09-05-graycode-eco-integrity.md index 3ea3c82c..2fd01356 100644 --- a/docs/plans/2026-09-05-graycode-eco-integrity.md +++ b/docs/plans/2026-09-05-graycode-eco-integrity.md @@ -35,7 +35,7 @@ --- -# Workstream A — Skills Registry +## Workstream A — Skills Registry **Problem.** `graycode skills search` cannot work today, and `graycode skills install GrayCodeAI/graycode-skills ` cannot work either. The link is broken at three independent layers, and fixing only the URL restores nothing. @@ -331,18 +331,18 @@ Append to `graycode-cli/internal/plugin/registry_test.go`: ```go func TestDefaultIndexURLIsPublished(t *testing.T) { - const want = "https://github.com/GrayCodeAI/graycode-skills/releases/download/registry-latest/registry.json" - if defaultIndexURL != want { - t.Fatalf("defaultIndexURL = %q, want %q", defaultIndexURL, want) - } - if strings.Contains(defaultIndexURL, "starling") { - t.Errorf("defaultIndexURL still references the renamed starling repo") - } + const want = "https://github.com/GrayCodeAI/graycode-skills/releases/download/registry-latest/registry.json" + if defaultIndexURL != want { + t.Fatalf("defaultIndexURL = %q, want %q", defaultIndexURL, want) + } + if strings.Contains(defaultIndexURL, "starling") { + t.Errorf("defaultIndexURL still references the renamed starling repo") + } } func TestFetchIndexParsesGeneratedShape(t *testing.T) { - // Byte-for-byte the shape graycode-skills/tools/update_registry.py emits. - const generated = `{ + // Byte-for-byte the shape graycode-skills/tools/update_registry.py emits. + const generated = `{ "version": 1, "skills": [ { @@ -358,22 +358,22 @@ func TestFetchIndexParsesGeneratedShape(t *testing.T) { ] } ` - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(generated)) - })) - defer srv.Close() - - rc := &RegistryClient{IndexURL: srv.URL, CacheDir: t.TempDir(), client: srv.Client()} - idx, err := rc.FetchIndex() - if err != nil { - t.Fatalf("FetchIndex: %v", err) - } - if len(idx.Skills) != 1 { - t.Fatalf("skills = %d, want 1", len(idx.Skills)) - } - if idx.Skills[0].Repo != "GrayCodeAI/graycode-skills" { - t.Errorf("Repo = %q, want the slug the installer clones from", idx.Skills[0].Repo) - } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(generated)) + })) + defer srv.Close() + + rc := &RegistryClient{IndexURL: srv.URL, CacheDir: t.TempDir(), client: srv.Client()} + idx, err := rc.FetchIndex() + if err != nil { + t.Fatalf("FetchIndex: %v", err) + } + if len(idx.Skills) != 1 { + t.Fatalf("skills = %d, want 1", len(idx.Skills)) + } + if idx.Skills[0].Repo != "GrayCodeAI/graycode-skills" { + t.Errorf("Repo = %q, want the slug the installer clones from", idx.Skills[0].Repo) + } } ``` @@ -381,22 +381,22 @@ Append to `graycode-cli/internal/plugin/marketplace_test.go`: ```go func TestNoPhantomDefaultMarketplaceSource(t *testing.T) { - for _, src := range DefaultMarketplaceSources() { - if strings.Contains(src.URL, "plugins-registry.json") { - t.Fatalf("default source %q points at plugins-registry.json, which nothing generates", src.Name) - } - } + for _, src := range DefaultMarketplaceSources() { + if strings.Contains(src.URL, "plugins-registry.json") { + t.Fatalf("default source %q points at plugins-registry.json, which nothing generates", src.Name) + } + } } func TestFetchAllWithNoSourcesReturnsEmptyNotError(t *testing.T) { - mc := &MarketplaceClient{Sources: nil, CacheDir: t.TempDir()} - entries, err := mc.FetchAll() - if err != nil { - t.Fatalf("FetchAll with no sources returned error: %v", err) - } - if len(entries) != 0 { - t.Fatalf("entries = %d, want 0", len(entries)) - } + mc := &MarketplaceClient{Sources: nil, CacheDir: t.TempDir()} + entries, err := mc.FetchAll() + if err != nil { + t.Fatalf("FetchAll with no sources returned error: %v", err) + } + if len(entries) != 0 { + t.Fatalf("entries = %d, want 0", len(entries)) + } } ``` @@ -434,7 +434,7 @@ In `graycode-cli/internal/plugin/marketplace.go`, replace the exported `DefaultM // on every `graycode plugin marketplace list`. Users register real sources // with `graycode plugin marketplace add `. func DefaultMarketplaceSources() []MarketplaceSource { - return nil + return nil } ``` @@ -493,74 +493,74 @@ Append to `graycode-cli/internal/plugin/registry_test.go`: ```go func TestDiscoverSkillDirs(t *testing.T) { - tests := []struct { - name string - layout []string // SKILL.md paths relative to the repo root - want []string // expected skill names - }{ - { - name: "flat layout", - layout: []string{"go-review/SKILL.md"}, - want: []string{"go-review"}, - }, - { - name: "agentskills.io skills/ layout", - layout: []string{"skills/go-review/SKILL.md"}, - want: []string{"go-review"}, - }, - { - name: "graycode-skills categories layout", - layout: []string{"categories/go/go-review/SKILL.md", "categories/python/pandas/SKILL.md"}, - want: []string{"go-review", "pandas"}, - }, - { - name: "ignores vendored and dot directories", - layout: []string{"go-review/SKILL.md", ".git/hooks/SKILL.md", "node_modules/pkg/SKILL.md"}, - want: []string{"go-review"}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - root := t.TempDir() - for _, rel := range tc.layout { - full := filepath.Join(root, rel) - if err := os.MkdirAll(filepath.Dir(full), 0o750); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(full, []byte("---\nname: x\n---\n"), 0o600); err != nil { - t.Fatal(err) - } - } - - got, err := discoverSkillDirs(root) - if err != nil { - t.Fatalf("discoverSkillDirs: %v", err) - } - if len(got) != len(tc.want) { - t.Fatalf("found %d skills %v, want %d %v", len(got), keysOf(got), len(tc.want), tc.want) - } - for _, name := range tc.want { - dir, ok := got[name] - if !ok { - t.Errorf("missing skill %q; got %v", name, keysOf(got)) - continue - } - if _, err := os.Stat(filepath.Join(dir, "SKILL.md")); err != nil { - t.Errorf("skill %q maps to %q which has no SKILL.md", name, dir) - } - } - }) - } + tests := []struct { + name string + layout []string // SKILL.md paths relative to the repo root + want []string // expected skill names + }{ + { + name: "flat layout", + layout: []string{"go-review/SKILL.md"}, + want: []string{"go-review"}, + }, + { + name: "agentskills.io skills/ layout", + layout: []string{"skills/go-review/SKILL.md"}, + want: []string{"go-review"}, + }, + { + name: "graycode-skills categories layout", + layout: []string{"categories/go/go-review/SKILL.md", "categories/python/pandas/SKILL.md"}, + want: []string{"go-review", "pandas"}, + }, + { + name: "ignores vendored and dot directories", + layout: []string{"go-review/SKILL.md", ".git/hooks/SKILL.md", "node_modules/pkg/SKILL.md"}, + want: []string{"go-review"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + for _, rel := range tc.layout { + full := filepath.Join(root, rel) + if err := os.MkdirAll(filepath.Dir(full), 0o750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, []byte("---\nname: x\n---\n"), 0o600); err != nil { + t.Fatal(err) + } + } + + got, err := discoverSkillDirs(root) + if err != nil { + t.Fatalf("discoverSkillDirs: %v", err) + } + if len(got) != len(tc.want) { + t.Fatalf("found %d skills %v, want %d %v", len(got), keysOf(got), len(tc.want), tc.want) + } + for _, name := range tc.want { + dir, ok := got[name] + if !ok { + t.Errorf("missing skill %q; got %v", name, keysOf(got)) + continue + } + if _, err := os.Stat(filepath.Join(dir, "SKILL.md")); err != nil { + t.Errorf("skill %q maps to %q which has no SKILL.md", name, dir) + } + } + }) + } } func keysOf(m map[string]string) []string { - out := make([]string, 0, len(m)) - for k := range m { - out = append(out, k) - } - sort.Strings(out) - return out + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out } ``` @@ -593,53 +593,53 @@ const maxSkillSearchDepth = 4 // On a duplicate skill name the shallowest path wins; ties keep the first // lexicographic match so the result is deterministic. func discoverSkillDirs(root string) (map[string]string, error) { - found := map[string]string{} - depthOf := map[string]int{} - - err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - rel, relErr := filepath.Rel(root, path) - if relErr != nil { - return nil //nolint:nilerr // an unrelatable path is simply skipped - } - if d.IsDir() { - if path == root { - return nil - } - name := d.Name() - if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" { - return filepath.SkipDir - } - if len(strings.Split(filepath.ToSlash(rel), "/")) > maxSkillSearchDepth { - return filepath.SkipDir - } - return nil - } - if d.Name() != "SKILL.md" { - return nil - } - dir := filepath.Dir(path) - if dir == root { - return nil // a top-level SKILL.md documents the repo, not a skill - } - name := filepath.Base(dir) - depth := len(strings.Split(filepath.ToSlash(rel), "/")) - if prev, ok := found[name]; ok { - if depthOf[name] <= depth { - return nil - } - _ = prev - } - found[name] = dir - depthOf[name] = depth - return nil - }) - if err != nil { - return nil, fmt.Errorf("scan skills: %w", err) - } - return found, nil + found := map[string]string{} + depthOf := map[string]int{} + + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, relErr := filepath.Rel(root, path) + if relErr != nil { + return nil //nolint:nilerr // an unrelatable path is simply skipped + } + if d.IsDir() { + if path == root { + return nil + } + name := d.Name() + if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" { + return filepath.SkipDir + } + if len(strings.Split(filepath.ToSlash(rel), "/")) > maxSkillSearchDepth { + return filepath.SkipDir + } + return nil + } + if d.Name() != "SKILL.md" { + return nil + } + dir := filepath.Dir(path) + if dir == root { + return nil // a top-level SKILL.md documents the repo, not a skill + } + name := filepath.Base(dir) + depth := len(strings.Split(filepath.ToSlash(rel), "/")) + if prev, ok := found[name]; ok { + if depthOf[name] <= depth { + return nil + } + _ = prev + } + found[name] = dir + depthOf[name] = depth + return nil + }) + if err != nil { + return nil, fmt.Errorf("scan skills: %w", err) + } + return found, nil } ``` @@ -658,22 +658,22 @@ Expected: PASS, all four subtests. In `graycode-cli/internal/plugin/registry.go`, replace the discovery block at lines 258-270. Delete the `skillsRoot` computation and the `os.ReadDir(skillsRoot)` call, and drive the existing loop from the map instead: ```go - // Discover skills in the cloned repo, whatever layout it uses. - discovered, err := discoverSkillDirs(tmpDir) - if err != nil { - return "", err - } - names := make([]string, 0, len(discovered)) - for name := range discovered { - names = append(names, name) - } - sort.Strings(names) + // Discover skills in the cloned repo, whatever layout it uses. + discovered, err := discoverSkillDirs(tmpDir) + if err != nil { + return "", err + } + names := make([]string, 0, len(discovered)) + for name := range discovered { + names = append(names, name) + } + sort.Strings(names) ``` Then change the loop header from `for _, e := range entries {` to `for _, name := range names {`, delete the `if !e.IsDir() { continue }` guard and the `name := e.Name()` line, and change the `srcSkill` assignment to: ```go - srcSkill := filepath.Join(discovered[name], "SKILL.md") + srcSkill := filepath.Join(discovered[name], "SKILL.md") ``` Leave the rest of the loop body, including the `skillName` filter, the trust checks, and the lockfile writes, exactly as they are. Add `"sort"` to the imports if it is not already present. @@ -714,7 +714,7 @@ Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" --- -# Workstream B — Cloud Wire Contract +## Workstream B — Cloud Wire Contract **Problem.** Two field names drifted between graycode-cli and graycode-platform. Both schemas on the worker are `.strict()` zod objects, so both calls are rejected outright. @@ -1104,7 +1104,7 @@ Expected: migration reports one statement batch applied, then both workers deplo --- -# Workstream C — Router Boundary Truth +## Workstream C — Router Boundary Truth **Problem.** This workstream fixes documentation and two defective guards. **There is no import violation to repair: all nine boundary guards pass today.** @@ -1266,9 +1266,9 @@ Expected: the list includes `boundary-graycode-router-engine`. Delete the exception block at `graycode-cli/internal/testaudit/audit_test.go:223-226`: ```go - if strings.HasPrefix(rel, "internal/provider/gateway/") && path == graycodeRouterModule+"/credentials" { - continue - } + if strings.HasPrefix(rel, "internal/provider/gateway/") && path == graycodeRouterModule+"/credentials" { + continue + } ``` And at `graycode-cli/internal/testaudit/package_boundaries_test.go:61-65`, the equivalent block keyed on `filepath.ToSlash(filepath.Dir(relFile)) == "internal/provider/gateway"`. @@ -1305,7 +1305,7 @@ Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" --- -# Workstream D — Naming Sweep +## Workstream D — Naming Sweep **Scope (approved):** documentation, user-visible strings, and code comments. Roughly 230 sites. `graycode-cli` is already clean: it contains **zero** occurrences of `hawk` outside generated GitNexus blocks. Marketing copy under `graycode-platform/apps/web` is out of scope; it is dated content and product-line branding. @@ -1444,34 +1444,34 @@ Create `graycode-router/setup/naming_test.go`: package setup import ( - "os" - "path/filepath" - "strings" - "testing" + "os" + "path/filepath" + "strings" + "testing" ) // TestNoLegacyHostNameInUserFacingStrings guards the three call sites that // print a command for the user to run. They named the old product. func TestNoLegacyHostNameInUserFacingStrings(t *testing.T) { - files := []string{ - filepath.Join("..", "catalog", "v1.go"), - filepath.Join("..", "setup", "status.go"), - filepath.Join("..", "runtime", "preflight.go"), - } - for _, f := range files { - data, err := os.ReadFile(f) // #nosec G304 -- fixed test fixture paths - if err != nil { - t.Fatalf("read %s: %v", f, err) - } - for i, line := range strings.Split(string(data), "\n") { - if !strings.Contains(line, `"`) { - continue - } - if strings.Contains(line, "hawk models refresh") || strings.Contains(line, "hawk will discover") || strings.Contains(line, "hawk refreshes") { - t.Errorf("%s:%d prints the legacy host name to the user: %s", f, i+1, strings.TrimSpace(line)) - } - } - } + files := []string{ + filepath.Join("..", "catalog", "v1.go"), + filepath.Join("..", "setup", "status.go"), + filepath.Join("..", "runtime", "preflight.go"), + } + for _, f := range files { + data, err := os.ReadFile(f) // #nosec G304 -- fixed test fixture paths + if err != nil { + t.Fatalf("read %s: %v", f, err) + } + for i, line := range strings.Split(string(data), "\n") { + if !strings.Contains(line, `"`) { + continue + } + if strings.Contains(line, "hawk models refresh") || strings.Contains(line, "hawk will discover") || strings.Contains(line, "hawk refreshes") { + t.Errorf("%s:%d prints the legacy host name to the user: %s", f, i+1, strings.TrimSpace(line)) + } + } + } } ``` @@ -1749,7 +1749,7 @@ Claude-Session: https://claude.ai/code/session_01MTUKadN91fcmuhxGWYVe2k" --- -# Execution Order +## Execution Order The workstreams are independent, but two ordering constraints are real: @@ -1767,7 +1767,7 @@ Recommended sequence, one PR per repo per workstream: --- -# Self-Review +## Self-Review **Coverage.** Every finding from the scouting pass maps to a task: registry publishing (A2), registry shape (A1), registry consumption (A3), install discovery (A4), device-login field drift (B1-B2), usage capability drift (B1-B2), BFF enum (B3), triplicate OpenAPI plus personal subdomain (B4), missing cloud endpoint documented as a known gap (B5), documented-vs-enforced boundary (C1), vacuous peer guard (C2), missing pre-push hook (C2), dead credentials exception (C2), legacy naming (D1-D4), wrong skill and category counts (D1), rename-corruption typos (D3), hard-coded personal path (D2).