Moves 5 related docs into docs/devops/gitea-runner/ to keep this
multi-doc workstream from colliding with future roadmaps and
delegation prompts in docs/devops/.
Renames:
HOSTINGER_GITEA_RUNNER_ROADMAP.md -> ROADMAP.md
HOSTINGER_GITEA_ACT_RUNNER_SETUP.md -> ACT_RUNNER_SETUP.md
GITEA_PACKAGES_PUBLISH_WORKFLOW.md -> PUBLISH_WORKFLOW.md
HOSTINGER_GITHUB_RUNNER_SETUP.md -> _PLAN_B_GITHUB_RUNNER.md
CODEX_DELEGATION_PROMPT.md -> (same name, moved)
All internal cross-links updated via sed sweep. Verified no stale
references remain.
Adds README.md in the new folder as the index + pattern doc for
future multi-doc workstreams (one-liner handoff, file map,
architecture summary).
Updated one-liner handoff path:
Read docs/devops/gitea-runner/CODEX_DELEGATION_PROMPT.md ...
Captures the exact bootstrap prompt to paste to Codex on the
Hostinger VM, plus a one-liner that just points Codex at the prompt
file (after Codex has the repo).
Also documents how to monitor Codex's progress from Cascade side
(grep roadmap-update commits) and how to recover if it gets stuck
(the checkbox state in the roadmap IS the resume pointer).
Adds HOSTINGER_GITEA_RUNNER_ROADMAP.md — a single execution tracker
that Codex on the Hostinger VM works through phase-by-phase, ticking
checkboxes and recording commit hashes as it goes.
Structure:
- 6 phases (P0 Pre-flight → P5 First real release) + P6 review handoff
- Each task: [ ] checkbox + Commit hash field + Status note
- Detail steps live in the two companion docs (act_runner setup +
publish workflow); the roadmap is the orchestrator
- Final report section Codex fills in when P0-P5 are complete
- Human review checklist (R1-R9) for verification after handoff
- Operating notes: commit message format, when to ask, never-do list
- Change log table Codex auto-appends to
Critical invariant repeated at P3.6 and P5.4: cross-Gitea SHA1
comparison must match. If it doesn't, Codex stops — it's the
load-bearing architectural guarantee that the dual-Gitea, no-sync-
script model rests on.
Also adds roadmap-pointer banners to the two companion docs
(HOSTINGER_GITEA_ACT_RUNNER_SETUP.md, GITEA_PACKAGES_PUBLISH_WORKFLOW.md)
so anyone landing there knows the master tracker exists.
Adds two new docs and a banner on the existing GitHub-runner doc.
WHY: the user already has Gitea Actions configured across all 20+
repos (.gitea/workflows/ci.yml). Building a parallel GitHub Actions
self-hosted runner pipeline is unnecessary work that also drags in
GitHub Organization migration pressure (with Vercel/Netlify pricing
side-effects on free tiers).
The canonical architecture instead:
- Each Gitea instance (corp Mac local + Hostinger VM) runs its own
act_runner.
- A single publish-packages.yml workflow lives in every package-
publishing repo.
- When the same git tag is pushed to both Giteas, each one builds
inside the same pinned Docker image (node:20-bookworm@sha256:...)
with the same lockfile, producing BYTE-IDENTICAL tarballs.
- No sync script is needed; the shared git tag IS the sync mechanism.
- Lockfile integrity hashes match across both registries, so corp Mac
and personal Mac + Hostinger prod all see the same packages.
New: HOSTINGER_GITEA_ACT_RUNNER_SETUP.md
- Codex-actionable prompt to install act_runner on the Hostinger VM
- Pre-flight checks (arch detection, Docker daemon, Gitea reachable)
- Idempotent user creation, SHA-verified binary download
- Docker mode runner config with labels mapping ubuntu-latest to
pinned Node image
- Smoke test + full E2E with throwaway @bytelyst/_runner-e2e-test
package
- The architectural invariant check: cross-Gitea SHA comparison —
same tag pushed to both must produce identical tarballs
- Monitoring (Gitea UI, API, systemd journal)
- Hardening, rollback, deliverables, guardrails, questions
New: GITEA_PACKAGES_PUBLISH_WORKFLOW.md
- The actual publish-packages.yml triggered by v* tags
- Docker image pinned by digest for build determinism
- pnpm@9.12.0 pinned, --frozen-lockfile, host-network container
- Token mounted as read-only secret file (not env var)
- Concurrency cancel-in-progress: false (never cancel a publish)
- Pack tarballs + SHA512 manifest as Gitea Release assets for audit
trail
- Two propagation strategies: reusable workflow (preferred) vs
sync-publish-workflow.sh script
- Operator runbook for cutting a release
- Failure-mode table + remediation
- Deliverables checklist
Updated: HOSTINGER_GITHUB_RUNNER_SETUP.md
- Added 'PLAN B' banner at the top
- Cross-links to the Gitea Actions docs
- Kept the doc intact as a valid alternative if priorities ever
shift to making GitHub Actions the publish driver
Adds the missing pieces revealed during review:
§1 Multi-repo registration decision — choose repo-level vs org-level
up-front. Default doc remains repo-level, but explicitly calls out
org-level as the scaling path for 20+ repos.
§2 Pre-flight check additions:
- Arch detection (x86_64 / aarch64) before downloading runner tarball
- github.com + objects.githubusercontent.com reachability check
- gh CLI auth status check (must be saravanakumardb1)
§4 Installation hardening:
- Step 1 is now idempotent (getent guards on useradd/usermod)
- Step 3 queries latest runner version via gh api (no more stale pin)
- Step 3 includes SHA256 verification of the downloaded tarball
against the release-notes manifest, with explicit STOP-if-mismatch
- Step 3 has REGISTRATION_URL var with commented Option A/B for
repo-level vs org-level scope
§5 Smoke test — added explicit git checkout/add/commit/push commands
for creating the runner/smoke branch (was implicit before).
§8 (renamed) — comprehensive org migration guide:
- Side-by-side table: personal account today vs under-an-org
- Bash loop to transfer all 18 repos via gh api
- git remote set-url commands for each local clone
- Post-migration org-level registration token fetch
- Workflow propagation strategies (reusable workflow vs sync script)
§9 (new) — Monitoring + observability:
- GitHub Actions tab per-repo + per-org workflow views
- Runner pool health (Settings → Actions → Runners) at repo + org level
- gh CLI commands for scripted monitoring (run watch, list, view, runners)
- Host-side journalctl + _diag/ inspection commands
§14 Questions — updated to ask about scope (repo vs org) first.
Section numbering shifted by +1 from §9 onward to make room for the
new Monitoring section.
Delegation prompt for the Codex agent running on the Hostinger VM to:
- Install a dedicated GitHub Actions self-hosted runner under gha-runner user
- Register it with saravanakumardb1/learning_ai_common_plat
- Run as a systemd service with auto-start
- Install Node 20 / pnpm 9 / gh CLI / Docker prerequisites
- Wire up local Gitea publish token
Includes full end-to-end validation that proves the actual publish
pipeline works:
- Creates a throwaway @bytelyst/_runner-e2e-test package
- Publishes to local Gitea
- Uploads tarball as GitHub Release asset
- Verifies Gitea registry returns the version
- Verifies pnpm install + require works from a clean directory
- Verifies the byte-identical-tarball invariant (sha256 match between
Gitea-served tarball and GitHub Release asset) — this is the key
guarantee that lockfiles will remain portable across corp Mac's
local Gitea after sync
- Documents cleanup of test artifacts
Plus pre-flight checks, hardening (systemd limits, log rotation,
workflow approval), scaling notes, deliverables checklist, guardrails,
rollback, and follow-up prompt list (publish-packages.yml,
bytelyst-sync script, SKILL doc).
Documents the local zsh chpwd hook + git credential helper that
auto-routes the gh CLI to the personal saravanakumardb1 account
whenever cwd is inside ~/code/mygh/, falling back to the keyring
active account elsewhere.
Captures the setup so it can be reproduced on a new laptop or by
another contributor with similar two-account needs. This is machine-
local config (lives in ~/.zshrc and ~/.gitconfig-personal), so it
intentionally lives in SKILLS/ rather than per-repo AGENTS.md.
Tail-end of the ts-any TODO-4 work uncovered a small class of false
positives the scanner still surfaced: ':any' that appears as TEXT inside
a string literal or JSX child, not as a TypeScript type annotation.
Examples:
const label = 'Energy: any'; // string content, not a type
<Badge>owner:any</Badge> // JSX text, not a type
Real TS ': any' annotations are followed by ',', ')', '=', ';', '>',
or end-of-line. Text occurrences are followed by alphanumeric / quote /
closing-tag delimiter characters \u2014 a clear distinguishing signal.
This commit adds a 10-line regex heuristic that skips occurrences where
':any' is followed by ' ', single quote, double quote, or '<'. The
companion AGENT_COMPLIANCE_ROADMAP.md entry for commit 79041714 already
listed this heuristic; the implementation just wasn't actually committed
at the time. This commit retroactively lands it so the working tree
matches the docs.
Verification: scripts/check-rule-violations.sh still emits 0 findings
across all 20 repos (no regression from the additional heuristic).
TODO-3 (commit 8ffe3616) added an optional 'logger' callback to MCPConfig
plus an exported McpLogger interface so consumers can route MCP output
to pino, Fastify request.log, structlog, etc., instead of the default
global console. The package had zero unit tests; the new interface
relied on type-system validation alone.
This commit adds packages/mcp-client/src/logger.test.ts (4 tests) to
cover the public contract introduced by TODO-3:
1. defaults to global console when no logger is provided
\u2014 verifies the '?? console' fallback in the constructor.
2. injected logger receives no spurious calls on early-return paths
\u2014 disconnect() when not connected is a no-op; logger must
not be invoked.
3. structural-typing acceptance test \u2014 a pino-shaped logger
(no-op methods) must construct cleanly. Guards the McpLogger
interface from accidental narrowing during future refactors.
4. variadic-args contract \u2014 McpLogger.info('msg', {ctx}, 42)
accepts trailing structured args; matches console + pino + Fastify.
The deeper integration paths (connect / callTool / readResource) spawn
StdioClientTransport subprocesses and aren't safely runnable in a unit
context; they're covered indirectly by consumers (admin-dashboard
uses the same client and has integration tests).
Result: mcp-client moves from 0 tests to 4 tests passing.
The DevOps admin preHandler read 'auth' as '(request as any).auth'.
The proper Fastify pattern is 'declare module' augmentation in
@bytelyst/fastify-auth, but the inline cast through 'unknown' is
sufficient for now and avoids touching the shared auth package.
Changed:
- 'const auth = (request as any).auth;' \u2192
'const auth = (request as unknown as { auth?: { role?: string } }).auth;'
Inline comment notes the cleaner 'declare module' alternative.
Final ecosystem state:
scripts/check-rule-violations.sh: 0 findings across all rules \u2713
web-hardcoded-hex: 0 \u2713
b5-hardcoded-product-id: 0 \u2713
b4-console-log: 0 \u2713
b4-swift-print: 0 \u2713
b4-python-print: 0 \u2713
ts-any-type: 0 \u2713
b7-emoji-in-code: 0 \u2713
Per user request: 'use the local Gitea and make sure all packages
in Gitea are up to date'.
Built all packages from learning_ai_common_plat/packages/* and ran
scripts/gitea/publish-outdated-packages.sh against the local Gitea
npm registry (http://localhost:3300/api/packages/bytelyst/npm/).
Manifest-based hash comparison flagged 4 packages whose built dist/
content has changed since the last published tarball:
@bytelyst/auth-ui 0.1.5 → 0.1.6
@bytelyst/config 0.1.7 → 0.1.8
@bytelyst/dashboard-shell 0.1.5 → 0.1.6
@bytelyst/mcp-client 0.1.0 → 0.1.1
All four bumped + published successfully. Remaining 60 packages
verified up-to-date. One package skipped by design:
@bytelyst/react-native-platform-sdk (RN — not in npm publish set)
Also incidentally fixed an mcp-client build break before this run:
stale dist/ + node_modules/.cache made tsc think MCPClient was
missing a 'log' property, even though the source had been correctly
refactored to use a private 'log: McpLogger' field. A clean
'rm -rf dist node_modules/.cache && pnpm build' resolved it; no
source changes needed.
Files updated:
- packages/auth-ui/package.json
- packages/config/package.json
- packages/dashboard-shell/package.json
- packages/mcp-client/package.json
- scripts/gitea/.publish-manifest.json (new content hashes)
After this commit, every published @bytelyst/* tarball in local
Gitea matches the source tree exactly.
Previously the @bytelyst/mcp-client package logged directly to the global
`console`, which made its output invisible to consumers running under
Fastify/pino or any structured logger. The scanner exempted the whole
package for console-log findings with a TODO-3 marker; this commit
resolves the marker.
packages/mcp-client/src/index.ts:
+ Added `McpLogger` interface (debug/info/warn/error, variadic) which
is structurally compatible with the global console, pino, and
Fastify's `request.log`.
+ Added optional `logger?: McpLogger` field on MCPConfig with a JSDoc
explaining when consumers should supply their own.
+ MCPClient now stores a `private readonly log: McpLogger` field
initialised from `config.logger ?? console` in the constructor.
+ All 17 internal logging sites switched from `console.X(...)` to
`this.log.X(...)`. Mapping: console.log \u2192 this.log.info (pino
does not have a 'log' method).
scripts/check-rule-violations.sh:
- Removed the blanket /packages/mcp-client/ exemption from the
console-log scanner (TODO-3 marker comment retained for history).
- The ts-any-type exemption stays \u2014 mcp-client still uses `any` at
the JSON-RPC payload boundary (different concern).
Verification:
packages/mcp-client \u2192 `pnpm build` clean (tsc).
`bash scripts/check-rule-violations.sh` \u2192 total still 88, no new
console-log findings (mcp-client is now genuinely clean instead of
blanket-exempted).
CI run 67 surfaced a real test failure:
src/modules/products/cache.test.ts:104
getAllProducts > returns all cached products
expected [ { id: 'lysnrai', …(11) }, …(2) ] to have a length of 2
but got 3
Root cause: cache.ts has a TEMPORARY_FALLBACK_PRODUCTS map (currently
just 'invttrdg') that getAllProducts() merges into its return value
on top of the loaded cache. The test fixture loads 2 products
(lysnrai, mindlyst), so the actual return is 3 — the test was
written before the fallback shim landed and never got updated.
Two ways to reconcile: (a) make the test reflect today's behaviour,
or (b) gut the fallback. The cache.ts comment explicitly marks
the fallback as 'TODO(platform): remove after creating the real
product …', so the right move is (a): keep the shim in place and
make the test enforce the documented contract.
- assertion now: toHaveLength(3) + .toContain('invttrdg')
- inline comment ties the expectation back to cache.ts so a
future cleanup removing the fallback will obviously need to
drop it back to 2
Verified locally:
pnpm vitest run cache.test.ts -> 8/8 pass
The previous fix (commit a4ee36b6) added '.pnpmfile.cjs' to the
globalIgnores() call at the END of the admin-web eslint config,
but flat-config v9 only applies ignores from the FIRST config
object that contains an 'ignores' key — every subsequent config
item is matched against the file before the late ignore is read.
That's why CI run 66 still failed with the same require() error
even after the dashboard-level ignore was in place.
Fix: declare an explicit '{ ignores: [...] }' at array index 0,
which is the documented eslint v9 pattern for skipping files
before any rule config attaches:
defineConfig([
{ ignores: ['**/.pnpmfile.cjs', '**/*.cjs'] }, // <-- now first
...nextVitals,
...nextTs,
{ rules: { ... } },
globalIgnores([ ... ]),
])
Verified locally:
cd dashboards/admin-web && npx eslint . -> 0 errors, 0 warnings
The previous root-level eslint.config.js .cjs ignore (commit
1be38bef) had no effect on CI because 'pnpm -r exec eslint .' walks
into each workspace and invokes the LOCAL eslint config. admin-web
has its own dashboards/admin-web/eslint.config.mjs (extending
eslint-config-next), which did NOT ignore .cjs files. So the same
.pnpmfile.cjs require() errors kept failing run 64 and run 65 with
identical output.
Fix at the consumer (dashboard) level so 'pnpm -r exec eslint' sees
the ignore regardless of which workspace it runs from:
globalIgnores([
...
'.pnpmfile.cjs',
'**/*.cjs',
])
Verified locally:
cd dashboards/admin-web && pnpm lint -> 0 errors
tracker-web has no .pnpmfile.cjs and no other .cjs configs, so its
eslint config does not need the same change yet — adding it would
be defensive but not required.
After the .venv ignore fix landed (commit 9be49acb), a different
lint error surfaced in CI run 64:
/dashboards/admin-web/.pnpmfile.cjs
1:12 error A `require()` style import is forbidden @typescript-eslint/no-require-imports
2:14 error A `require()` style import is forbidden @typescript-eslint/no-require-imports
`.pnpmfile.cjs` is a pnpm hook that MUST be CommonJS; it cannot
use ESM imports because pnpm loads it via Node's require() and the
file is not part of the TypeScript source tree at all. The
@typescript-eslint/no-require-imports rule should never apply to
*.cjs files.
Added two ignore patterns to the root config:
'**/.pnpmfile.cjs'
'**/*.cjs'
The broader '*.cjs' glob also covers any future CommonJS config
files (next.config.cjs, postcss.config.cjs, etc.).
Three consecutive commits to eslint.config.js (9be49acb, 1035b730,
7ae60852) failed to trigger any new common-plat CI runs even after
`git push origin main && git push gitea main`. Root cause: the
workflow had a 'paths:' filter that only matched packages/**,
services/**, dashboards/**, lockfiles, and a few specific
root manifests — `eslint.config.js` and the workflows themselves
were NOT in the list, so Gitea silently skipped every push that
only touched those files.
Added the two missing globs:
- 'eslint.config.js'
- '.gitea/workflows/**'
Both are demonstrably CI-relevant — eslint.config.js drives the
lint step that's currently failing, and workflow edits obviously
need to trigger a fresh run.
This commit edits a workflow file, so it is itself covered by the
new path glob and should trigger a run on push.
Empty commit '1035b730' and the earlier eslint.config fix
'9be49acb' did not trigger new CI runs on this Gitea instance —
suspect Gitea's push-event coalescing during the runner backlog
caused by parallel fastgap jobs. Adding a one-line comment to
eslint.config.js to force a content-different push and a fresh
run.