Skip to content

fix(setup): make gstack skills discoverable and usable in Factory Droid#660

Open
morluto wants to merge 5 commits intogarrytan:mainfrom
morluto:droid_setup_fixes
Open

fix(setup): make gstack skills discoverable and usable in Factory Droid#660
morluto wants to merge 5 commits intogarrytan:mainfrom
morluto:droid_setup_fixes

Conversation

@morluto
Copy link
Copy Markdown
Contributor

@morluto morluto commented Mar 30, 2026

Problem

Factory Droid support is currently broken for gstack installs via:

cd ~/gstack && ./setup --host factory

The generated skills exist on disk, but Droid cannot reliably discover or load them. In practice this means gstack skills may not appear in the / menu, and the install layout is not compatible with Droid's expected discovery and registry model.

Reproduction

  1. Install gstack for Factory Droid:
    cd ~/gstack && ./setup --host factory
  2. Restart Droid
  3. Type / — no gstack skills appear (debug, fix, test, qa, etc. are missing)
  4. Check the installed entries:
    ls ~/gstack/.factory/skills/    # generated skill directories exist in the repo
    ls -l ~/.factory/skills/        # gstack entries exist, but as absolute symlinks into ~/gstack
    ls ~/.agents/skills/            # empty — skills not copied here

Root Cause: Three Bugs in link_factory_skill_dirs()

Bug 1: Wrong target for installed skill contents

The original install did create entries in ~/.factory/skills/, but they pointed to the wrong place for discovery. gstack generated Factory skills in ~/gstack/.factory/skills/ and then linked ~/.factory/skills/ entries back into that project directory, while Droid expects the canonical skill contents to live under ~/.agents/skills/.

Bug 2: Absolute symlinks

gstack created absolute symlinks in ~/.factory/skills/:

ln -snf /Users/will/gstack/.factory/skills/gstack-qa ~/.factory/skills/gstack-qa

Droid follows only relative symlinks (../../.agents/skills/{skill}). Absolute paths are ignored.

Bug 3: Wrong sourceType in lockfile

gstack registered entries with sourceType: "local":

{
  "sourceType": "local",
  "sourceUrl": "file:///Users/will/gstack/.factory/skills/gstack-qa"
}

Droid's lockfile reader ignores sourceType: "local" entirely. Zero of the 57 working skills use this value — all use sourceType: "github".

How Droid Discovers Skills (Two-Tier System)

Droid startup
    │
    ├── Tier 1: Scans ~/.factory/skills/
    │       ├── Follows relative symlinks ──→ ~/.agents/skills/gstack-factory*/SKILL.md
    │       └── Ignores: absolute symlinks and project-local targets
    │
    └── Tier 2: Reads ~/.agents/.skill-lock.json (if present)
            ├── sourceType: "github"  ──→ loads skill ✓
            └── sourceType: "local"  ──→ ignores skill ✗

A skill appears in / when both conditions are met:

  1. Relative symlink exists in ~/.factory/skills/ pointing to the isolated Factory copy in ~/.agents/skills/gstack-factory*
  2. Lockfile entry keeps the public skill name (for example gstack-qa) with sourceType: "github" in ~/.agents/.skill-lock.json

The Fix (4 commits)

Commit 1: Use relative symlinks in ~/.factory/skills/

Changed from absolute paths to the pattern all working skills use:

ln -snf "../../.agents/skills/$skill_name" "$target"

This commit also removes the old skip for the root gstack skill, so that directory can now be linked like the rest of the skill set.

Commit 2: Copy skills to ~/.agents/skills/

Droid reads skill files from ~/.agents/skills/. Symlinks pointing into ~/gstack/.factory/skills/ do not work — must be real directories.

cp -r "$skill_dir" "$HOME/.agents/skills/$skill_name"

This means Factory now reads a copied snapshot from ~/.agents/skills/, not directly from the repo checkout. If you edit a skill in ~/gstack, rerun ./setup --host factory to refresh Droid's copy.

Commit 3: Register with sourceType: "github"

Updated the Python lockfile registration script to use the only sourceType Droid actually loads:

{
  "source": "gstack/gstack-qa",
  "sourceType": "github",
  "sourceUrl": "file:///Users/will/.agents/skills/gstack-qa"
}

Commit 4: Isolate Factory copies and bootstrap the lockfile

The first three fixes made Droid discovery work, but they still wrote Factory-generated skill files into the shared ~/.agents/skills/gstack-* namespace. That risked clobbering non-Factory installs that also use ~/.agents/skills/.

The final fix isolates Factory-owned copies under gstack-factory* names while keeping the public Droid-facing names unchanged:

cp -r "$skill_dir" "$HOME/.agents/skills/gstack-factory-qa"
ln -snf "../../.agents/skills/gstack-factory-qa" ~/.factory/skills/gstack-qa

It also bootstraps ~/.agents/.skill-lock.json on first install instead of skipping registration on clean machines:

{
  "version": 3,
  "dismissed": {},
  "lastSelectedAgents": [],
  "skills": {}
}

The lockfile still registers the public skill name (gstack-qa) so Droid shows the expected /qa command, but the sourceUrl now points at the isolated Factory copy:

{
  "source": "gstack/gstack-qa",
  "sourceType": "github",
  "sourceUrl": "file:///Users/will/.agents/skills/gstack-factory-qa"
}

Validation

After applying the fix:

# 1. Install
cd ~/gstack && ./setup --host factory

# 2. Verify filesystem layout
ls -l ~/.factory/skills/gstack-qa
# should show: ../../.agents/skills/gstack-factory-qa

test -d ~/.agents/skills/gstack-factory-qa && echo "copied"
# should print: copied

# 3. Verify lockfile
python3 -c "
import json
with open('$HOME/.agents/.skill-lock.json') as f:
    lock = json.load(f)
entry = lock['skills']['gstack-qa']
print(entry['sourceType'], entry['sourceUrl'])"
# should print: github file:///.../gstack-factory-qa

# 4. Restart Droid
# Type /qa in Droid — skill now appears ✓

Closes #661.

morluto added 4 commits March 30, 2026 17:29
…roid

## Problem

Droid scans ~/.factory/skills/ for skill directories. It follows *relative*
symlinks pointing to ../../.agents/skills/{skill}, but ignores absolute paths.

When setup ran with --host factory, it created absolute symlinks:

  ln -snf /Users/will/gstack/.factory/skills/gstack-qa ~/.factory/skills/gstack-qa

Droid ignored these entirely — no skills appeared in the '/' menu.

## Root Cause

Droid's filesystem scanner only resolves relative symlinks. Absolute symlinks
pointing to paths outside ~/.agents/skills/ are silently skipped.

## Reproduction

1. Clone gstack: git clone https://github.com/garrytan/gstack.git ~/gstack
2. Run setup: cd ~/gstack && ./setup --host factory
3. Check symlinks: ls -la ~/.factory/skills/ | grep gstack
4. Observe: gstack symlinks are absolute paths — WRONG
5. In Droid, type '/' — no gstack skills visible

## Fix

Change symlink creation from absolute to relative paths:

  ln -snf "../../.agents/skills/$skill_name" "$target"

Also removes the 'gstack' skip that prevented linking the root skill.

## Validation

After fix:
- ls ~/.factory/skills/gstack-qa points to '../../.agents/skills/gstack-qa'
- Droid '/' menu shows gstack skills after restart
- Verified all working skills (debug, fix, test) use same relative pattern
## Problem

Skills were installed to ~/gstack/.factory/skills/ (a project subdirectory).
Droid never scans this location — it only scans ~/.factory/skills/ and reads
skill files from ~/.agents/skills/.

Even with correct relative symlinks in ~/.factory/skills/, Droid could not load
skills because the actual skill files didn't exist in ~/.agents/skills/.

## Root Cause

Droid's skill loading requires skill files to exist in ~/.agents/skills/.
Symlinks in ~/.factory/skills/ point to skill locations, but the files must
actually be present at the target.

## Reproduction

1. Run: cd ~/gstack && ./setup --host factory
2. Check: ls ~/.agents/skills/ | grep gstack
3. Observe: no gstack directories — SKILL.md files are in ~/gstack/.factory/skills/
4. Droid reports skills as unavailable

## Fix

Copy skills as real directories into ~/.agents/skills/:

  mkdir -p "$HOME/.agents/skills"
  for skill_dir in "$factory_dir"/gstack*/; do
    cp -r "$skill_dir" "$HOME/.agents/skills/"
  done

This ensures Droid can read the actual SKILL.md files.

## Validation

After fix:
- ls ~/.agents/skills/ shows all 31 gstack skill directories
- Each directory contains SKILL.md and supporting files
- Droid can read skill metadata from ~/.agents/skills/
…=github

## Problem

Droid maintains a skill registry at ~/.agents/.skill-lock.json. It ONLY loads
entries where sourceType=github. Entries with sourceType=local are silently ignored.

The old setup registered skills with sourceType=local — producing dead lockfile
entries that Droid never read.

## Root Cause

Droid's lockfile reader checks 'sourceType' to determine if a skill should be
loaded. No working Droid skill uses sourceType=local — it's a no-op.

## Reproduction

1. Run: cd ~/gstack && ./setup --host factory
2. Check lockfile: python3 -c "import json; l=json.load(open('/Users/will/.agents/.skill-lock.json')); print({k:v['sourceType'] for k,v in l['skills'].items() if 'gstack' in k})"
3. Observe: all gstack entries have sourceType=local — NOT LOADED
4. Compare: working skills (debug, fix, test) have sourceType=github

## Fix

Register skills with sourceType=github in ~/.agents/.skill-lock.json:

  lock['skills'][entry] = {
    'source': 'gstack/' + entry,
    'sourceType': 'github',
    'sourceUrl': 'file://' + os.path.join(agents_skills, entry),
    ...
  }

This matches the format of all working Droid skills.

## Validation

After fix:
- python3 -c "import json; l=json.load(open('/Users/will/.agents/.skill-lock.json')); print(sum(1 for v in l['skills'].values() if v.get('sourceType')=='github'))"
- Shows count of github-sourced skills includes gstack
- Droid '/' menu shows gstack skills (e.g. /qa)
- Skill name comes from 'name:' field in SKILL.md, not directory name
## Problem

Factory install wrote Droid-specific skill files directly into ~/.agents/skills/gstack-*. That path is also the shared SKILL.md convention for non-Factory hosts, so installing for Droid could clobber an existing Gemini/Cursor-style gstack install with Factory-only preambles and frontmatter.

The same path also skipped the registry write on clean machines. If ~/.agents/.skill-lock.json did not exist yet, setup warned, skipped registration, and still printed a success message. That meant the branch fixed upgrades better than first-time installs.

## Root Cause

link_factory_skill_dirs() copied each Factory-generated skill into ~/.agents/skills using the public gstack-* names, then pointed ~/.factory/skills symlinks at those shared names. This assumed the shared ~/.agents/skills namespace was safe to overwrite, but the generated Factory skill files are not equivalent to the non-Factory variants.

The lockfile path also assumed Droid had already created ~/.agents/.skill-lock.json. On a clean profile there was no bootstrap step, so the registration code never ran.

## Fix

Copy Factory-generated skills into isolated ~/.agents/skills/gstack-factory* directories instead of overwriting the shared gstack-* names. Keep the public ~/.factory/skills/gstack-* symlink names stable, but point them at the isolated Factory copies.

Bootstrap ~/.agents/.skill-lock.json on first install with the minimal top-level shape Droid expects, then register only the Factory-owned public skill names while pointing sourceUrl at the isolated copy directories. Preserve existing installedAt values when rewriting those entries.

Also add setup validation tests that lock in the isolated copy naming, clean lockfile bootstrap, and the lockfile mapping between public skill names and isolated Factory copy paths.

## Validation

- bun test test/gen-skill-docs.test.ts
- bash -n setup
@morluto morluto changed the title fix(setup): make gstack skills discoverable by Factory Droid fix(setup): make gstack skills discoverable and usable in Factory Droid Mar 30, 2026
@morluto
Copy link
Copy Markdown
Contributor Author

morluto commented Apr 2, 2026

@garrytan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Factory Droid skill discovery is broken for gstack installs

1 participant