W Workflow Skills Wiki
Home Wiki Index GitHub

Install & Hub (harness)

From Workflow Skills Wiki, the living architecture & domain knowledge base

> Provenance: bin/cli.js, bin/install-rules.js, bin/skill-dependencies.json, .agents/skills/ws-shared/runtime/hub-layout.json, root AGENTS.md ยง Skill SoT, living synthesis of specs 0002, 0007, 0010, 0013, 0017, 0027, 0032, 0037, 0042, 0052, 0058, 0061, 0066, 0069, 0070, 0071, 0083, 0100-us-351, 0106, 0115-configurable-hub-root, 0119-us-389, 0132-us-429, 0137-subagent-task-dispatch, 0140-simplify-skill-versioning, 0149-us-455, 0150-us-457, 0169-us-488, 0171-us-497-link-integrity, 0173-us-492, 0172-us-498-install-mode-reporting.

Feature [edit]

The harness ships portable skill bodies through a package installer that supports Workflows-only and Full package selections, project-local installs, and optional global installs under $HOME/.agents/skills. Canonical skill content lives only under .agents/skills/ws-* in the upstream source tree; consumers receive managed copies on install and update. Hybrid execution resolves skill bodies local-first then {globalSkillsRoot}, while project $PWD/.ws/config.json always overrides global hub defaults. The consumer hub splits into installer-owned runtime/ (contracts, scripts, schemas), setup-only templates/ (seeds), and consumer-owned root files (config.json, STACK.md, MEMORY.md, CHANGELOG.md), classified by hub-layout.json. Upstream authoring prose in root AGENTS.md stays separate from the consumer-portable hub contract in .ws/AGENTS.md. Secondary global host targets stay minimal: Claude Code (~/.claude/skills) and OpenAI Codex (~/.codex/skills) receive per-skill folder projections, while Gemini CLI and Antigravity IDE are configured declaratively through ~/.gemini/config/skills.json, where the installer upserts one entry pointing at ~/.agents/skills with the regular-expression filter include_only: ["^ws-.*"]. On Windows the target host's scanner skips ReparsePoints (directory junctions and symlinks) and its slash-command index reads only the physical tree, so there the installer additionally projects every ws-* skill as a real physical directory copy under ~/.gemini/config/skills/, replacing any pre-existing junction or symlink; on POSIX the declarative-only strategy is kept because directory symlinks are followed and copies would only duplicate the tree.

How it works [edit]

Authors hash and publish only from .agents/skills/ in the upstream package. Skill bodies never name host products, never hardcode consumer artifact paths, and never ship legacy-path shims; install and update deliver the latest layout only. Config resolution follows explicit override, then project-local hub, then global fallback. Global installs write only the minimal consumer bundle and never copy runtime/, templates/, manifests, or memory files into the global tree as consumer data.

Package selection auto-selects transitive dependencies when a skill is chosen, and deselect never silently drops dependents. Retired skill ids migrate idempotently to canonical ids during update. Unpackaged external companions such as ws-memo and ws-session-tracking appear in externalSkills, are never vendored, and are never required for core workflow installs. Consumer mode audits ws-shared/AGENTS.md as the hub entrypoint; no skill or installer may require a repo-root .agents/AGENTS.md. The rules.harness path resolves through a thin local pointer or documented global fallback.

The Gemini entry upsert is idempotent and non-destructive: unrelated entries and inherits blocks survive; a matching unrestricted entry (no include_only) is never narrowed; and when a filtered entry already exists the owned pattern is reconciled to the single ^ws-.* regular expression โ€” the legacy shell-glob literal ws-* is stripped โ€” and appended only when absent, so repeat upserts leave exactly one entry per resolved skills path and never a duplicate. A malformed or wrongly shaped skills.json is backed up to <file>.bak.<timestamp> and reinitialized to a safe structure. Legacy cleanup under ~/.gemini/config/skills/ is links-only by default: it repairs stale ws-* reparse points (junctions and symlinks, including dangling ones) while preserving the physical ws-* directory copies the Windows projection deliberately created and never touching non-ws-* third-party skills. Global uninstall removes only the owned pattern from the managed entry, drops the entry entirely when no owned paths remain and no ws-* skills stay installed, and a partial uninstall keeps the entry plus the physical copies while still sweeping stale links; only a full uninstall passes includePhysical: true so the owned ws-* physical copies are removed along with the entry.

Secondary targets are auto-detected with read-only probes: an existing host root directory (for example ~/.gemini from Antigravity), the target skills directory, or the target config file counts as consent, while regular files at a host root never do. Auto-detected targets are best-effort, so a failing projection warns and continues instead of aborting the explicitly requested canonical install, and a bare update --global re-detects them so previously unrecorded targets heal. Projection clears broken reparse points first: projectSkillToTarget uses lexical existence checks so dangling Windows junctions or symlinks are removed before symlink creation or copy fallback, avoiding EEXIST and ENOENT failures across install and update.

Interactive global install and update always present the host-target picker (canonical plus claude, codex, and gemini) with recorded and auto-detected targets pre-selected, so pressing Enter keeps the current set. Non-interactive runs (piped stdin or --yes, with update gaining --yes) skip the prompt and reuse recorded targets before auto-detecting pre-existing host directories. An explicit --targets value, even canonical alone, skips both the prompt and auto-detection for that run. Copy-versus-symlink default follows the mode recorded in installed-skills.json, keeping repeat runs consistent with the first install choice.

Naming law requires every id containing spec to match ^ws-spec-, with identical strings across folder name, name: frontmatter, dependency graph keys, and router rows. Subagent projection via compile_host_subagents.cjs is disabled by default, compiled agents set disable-model-invocation: true, and unsupported hosts fall back to generic dispatch.

Node 22 is the only runtime any shipped skill, installer entry, or package.json script may depend on. Every packaged Python helper was rewritten as CommonJS .cjs (which require() shared helpers regardless of the root "type": "module") or deleted, provider intents kept their names while only the launched file changed, and recipes invoke skill helpers with an explicit node launcher while bash survives only for thin host adapters that locate Node and exec a .cjs. Consumers install and run the package with Node 22 and nothing else. The authoring contract in root AGENTS.md and SKILL_AUTHORING.md forbids new .py files, and ws-check-harness reports critical (python-bytecode-shipped) when any .py, .pyc, or .pyo file โ€” including __pycache__/ residue โ€” appears under .agents/skills/ or bin/, so the second runtime cannot drift back in.

The upstream authoring hub and the consumer hub are semantically separated and mechanically enforced. Repo-root AGENTS.md owns the Skill SoT, the dogfood session contract, the global-vs-local invoke tie-break, and the maintainer ship checklist, while {skillsRoot}/ws-shared/runtime/AGENTS.md (global fallback {globalSkillsRoot}/ws-shared/runtime/AGENTS.md; thin entrypoint {sharedDir}/AGENTS.md) carries only consumer-portable obligations behind an explicit consumer-hub identity banner. {skillsRoot}/ws-shared/runtime/CATALOG.md keeps the consumer task router and the external-dependencies mirror and carries no upstream maintainer tables. The installer copies the managed ws-shared/ tree (runtime + templates) into the skills install only; the consumer hub (.ws/) holds consumer-owned config, the generated entrypoint, and installer metadata, never runtime/ or templates/. ws-check-harness fails the upstream audit when the consumer hub contains any denylisted upstream-only phrase, and the context-budget gate caps {skillsRoot}/ws-shared/runtime/AGENTS.md at 14,000 bytes, so size pressure and content rules together keep the two hubs from reconflating.

The consumer shared-data folder lives at {projectRoot}/.ws with the same tree the previous skills-tree location held: consumer-owned config.json, STACK.md, and memory/changelog files beside installer-owned runtime/ and setup-only templates/, classified by runtime/hub-layout.json. The folder is committable by default and never git-ignored by the installer. Path resolution is token-strict everywhere: {skillsRoot} expands as explicit override, then project .agents/skills, then {globalSkillsRoot}; {sharedDir} is always the project .ws, whose config.json wins over any global hub config; consumer-facing prose and scripts always resolve hub and skill-tree locations through {sharedDir} / {skillsRoot} tokens instead of hardcoded paths, and bare ws-shared/ shorthand is banned from link targets. A config-dependent skill invoked globally without a project hub fails closed with a pointer to ws-configure-project instead of silently reading global config as project config.

The shared hub root is relocatable through one resolver instead of scattered path logic. A single module under {skillsRoot}/ws-shared/runtime/scripts/ derives the hub root from bootstrap discovery of <repo>/.ws/config.json, then the configured pathTokens.sharedDir, then the .ws default, and the installer, the autoload configurator, the skill seeder, and the hub-layout reads all call it. Install, update, uninstall, and migration therefore write hub artifacts (ignore rules, entrypoint pointer, generated autoload rows, manifests, STACK.md) under the configured root with integrity records kept consistent, while generated autoload rows and link prefixes render against the same root so the renderer never reverts them.

A fresh consumer hub is usable without hand-copying files from the global install. ws-configure-project and the install bootstrap seed, missing-only, the files the hub layout requires under the effective hub root: STACK.md from templates/STACK.md.example, the hub AGENTS.md and autoload.md pointers with links rewritten for that root, and the hub .gitignore from templates/hub.gitignore. Existing consumer bytes such as config.json or a maintained STACK.md are never rewritten, and a second run only adds what is still missing. MEMORY.md, memory/, and CHANGELOG.md stay at their configured rules.memoryDir / rules.changelogFile paths and are created on first use, installer metadata stays installer-written, and managed runtime/ and templates/ are never copied into the hub. The harness no longer fails solely on missing hub docs when a nested hub is configured.

Individual subagent tasks can be dispatched on demand outside an orchestrator. workflow-skills dispatch --subagent <name> --task "<directive>" [--payload <json>] [--json] and the async dispatchSubagentTask({ subagent, task, payload }) API share one module. Inputs are validated at the boundary (malformed JSON and path traversal are rejected), execution resolves through an explicit executor or the configured defaults.hostAdapter.cliTemplate runner, and a host with no runner returns a structured failure instead of a phantom success.

Package versioning is centralized in one authored file, .agents/skills/ws-shared/version.json, whose single version property is the canonical release semver. A release bump edits that file once, and the build synchronizes every derived surface โ€” package.json, both dependency manifests' packageVersion, the site footer, and telemetry provenance โ€” without touching skill bodies. Packaged SKILL.md frontmatter carries no version field, and no supported build path can stamp one. The integrity generator binds the canonical version into every packaged skill digest and the aggregate package digest, so a version bump alone changes all digests; verification fails closed when version.json is missing, malformed, or stale against the manifest. The installer copies and hashes version.json for project-local and global installs, and a consumer missing the file gets an explicit outdated-install diagnostic instead of a silent fallback version.

The hub install phase runs at most once per install or update run. afterSkillCopy seeds the hub while the skill loop copies ws-self-learning โ€” so a memory-only install still works โ€” and the drivers also ensure the hub after the loop, so a single run reached the hub twice and wrote the consumer config snapshot twice. One run-scoped latch inside the hub installer now covers every call site, and the run drivers reset it at entry, so project and global scope alike print one hub status block and one backup line.

The relocated hub-root autoload.md rewrites every bare link target that names a runtime sibling shipped beside the source file. The source document keeps its honest same-directory form (](tools.md), ](AGENTS.md), ](host-capability-tokens.md)) because those targets resolve where the file lives, and the install-time renderer is what relocates them: the bounded filename registry MANAGED_RUNTIME_SIBLING_FILES lists AGENTS.md, CROSS-PLATFORM.md, config-resolution.md, gates.md, host-capability-tokens.md, host-dispatch.md, scm-provider-contract.md, setup.md, and tools.md, so the skills-install-scope autoload emits runtime/<file> while the project-hub autoload emits a hub-relative link into the managed runtime directory. Both renderers โ€” renderConsumerAutoloadText in bin/cli.js and renderConsumerAutoload in the project configurator โ€” carry the same registry, and a wiring guard fails when only one of the two lists is edited. Anything outside the registry is left untouched, a runtime file missing from the resolved runtime directory keeps the fail-closed {globalSkillsRoot} token instead of inventing a path, and the rewrite is idempotent, so an update converges a stale installed autoload and a repeat run leaves the bytes unchanged.

The config.json.bak snapshot is pre-change state, not a per-run copy. The backup write preserves an existing backup whose bytes differ from the live config โ€” that file is the only record of the pre-update state โ€” and writes a fresh snapshot only when no differing backup exists; a live config that cannot be parsed is backed up from its raw bytes and left unmodified, and an unreadable packaged template leaves the live config alone. The snapshot is written before the live rewrite, so a run that changes the config bytes always leaves backup and live file differing, and an idempotent re-run keeps the same rollback point.

The test suite stays side-effect free against the live hub. Every editor invocation from tests passes an explicit isolated -ConfigPath pointing at a temp copy, diagnostic-only runs perform no writes and leave no .bak behind, PowerShell path literals are quoted so suite failures clean their fixtures, and the hub-mutation guard hashes backup content and runs on the failure path as well as the happy path so a green run never flips delivery-artifact flags in the repository's own .ws/config.json.

Missing repo-relative skill paths resolve at read time instead of materializing links. resolve_skill_path.cjs tries the project path first and falls back to the same relative path under {globalSkillsRoot}; it never creates a junction, symlink, or copy in the consumer repo, so git status stays clean. A path missing on both sides โ€” or with traversal or absolute form โ€” fails closed with a named error. ws-version reports which tree served the session: installScope (global vs project-local), the absolute loaded skill directory, the canonical packageVersion, and the stored hub path tokens, read-only with no writes or network calls.

The install-mode detector reports scope and version truthfully instead of guessing. It compares the resolved local and global skills roots as absolute canonical paths โ€” separator style, trailing separators, dot segments, and symlink or junction aliases never register as distinct roots, and case-only differences collapse on case-insensitive filesystems โ€” so a coincident tree reports global with a single scan root and an informational same-directory note, never hybrid with cross-tree duplicate guidance; genuinely distinct trees holding skills still report hybrid, and upstream package markers still win regardless of coincidence. The global version resolves from the canonical hub file {globalSkillsRoot}/ws-shared/version.json first, then the projection manifest packageVersion, then package-owned frontmatter in representative-id order, reporting null with null drift when no package-owned source yields a value โ€” external companion and consumer-authored frontmatter never vote. Drift against the package version stays informational at exit 0, and same is reported when the versions match.

Backend [edit]

The installer entrypoint is bin/cli.js with flags such as --global, --targets, and --symlink. bin/skill-dependencies.json carries the install graph, packageVersion (synchronized from ws-shared/version.json), and externalSkills. bin/install-rules.js implements the target registry and HUB_WHITELIST. retired_artifacts.cjs tracks RETIRED_HUB_FILES and RETIRED_DEFAULTS_KEYS. Multi-host global installs keep $HOME/.agents/skills canonical and project per-skill junctions on Windows or symlinks on POSIX into companion host skill directories without clobbering third-party skills. Gemini declarative configuration runs through getGeminiSkillsJsonPath, readGeminiSkillsJson, upsertGeminiSkillsJsonEntry, removeGeminiSkillsJsonEntry, shouldProjectGeminiAsCopy, and cleanupLegacyGeminiSkills in bin/install-rules.js, with the owned-pattern constant OWNED_GEMINI_PATTERNS (^ws-.* plus the legacy ws-* glob) driving both upsert reconciliation and uninstall removal; GLOBAL_HOST_TARGETS carries the configSubpath for the gemini target and detectExistingSecondaryTargets drives the consent probes. The shared promptSecondaryGlobalTargets picker with the computeTargetPreselectIds helper serves both install and update, and the CLI help documents the prompt, --targets, and --yes behavior. test/test-install.js covers skills.json creation, legacy-glob-to-regex migration, idempotency, links-only versus physical legacy cleanup, custom-entry preservation, corrupt-JSON recovery, and uninstall, and isolates the gemini skills.json path so test runs never read or write the real user profile.

Consumer configuration uses pathTokens (skillsRoot, sharedDir), providers.active and providers.scm, plans.specsDir and optional plans.wikiDir, rules.* guardrail paths, and defaults.specializedSubagents. Consumer-owned scripts resolve the hub from $PWD via resolve_consumer_root.cjs, never from __file__ inside a managed skill copy. Hub-root resolution is single-sourced through resolve_hub_root.cjs, with bootstrap_runtime.cjs and cli_spawn.cjs sharing the same bootstrap so installer, configurator, and generator agree on one root, seed_consumer_hub.cjs (in ws-configure-project) performs the missing-only hub seed, dispatch_subagent_task.cjs backs the dispatch CLI command and API, and resolve_proof_of_work.cjs wires the optional evidence step through the same contract.