Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 81 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
enable-cache: true
cache-dependency-glob: pyproject.toml

- name: Verify clean checkout has no generated package resources
- name: Verify clean checkout package resource ownership
shell: python
run: |
import subprocess
Expand All @@ -54,12 +54,27 @@ jobs:

generated = (
Path("fsq_agent/adapters/control_plane/static"),
Path("fsq_agent/resources"),
)
present = [str(path) for path in generated if path.exists()]
if present:
raise SystemExit(f"Generated package resources are unexpectedly tracked: {present}")

package_resources = (
Path("fsq_agent/config/config.android.yaml"),
Path("fsq_agent/config/config.web.yaml"),
Path("fsq_agent/config/config.windows.yaml"),
Path("fsq_agent/config/config.macos.yaml"),
Path("fsq_agent/config/config.example.yaml"),
Path("fsq_agent/resources/skills/android-harness.md"),
Path("fsq_agent/resources/skills/web-harness.md"),
Path("fsq_agent/resources/skills/windows-harness.md"),
Path("fsq_agent/resources/skills/macos-harness.md"),
Path("fsq_agent/resources/skills/automation-basics.md"),
)
missing = [str(path) for path in package_resources if not path.is_file()]
if missing:
raise SystemExit(f"Tracked package resources are missing: {missing}")

- name: Install quality dependencies
run: uv sync --extra dev --reinstall-package fsq-agent

Expand Down Expand Up @@ -102,19 +117,34 @@ jobs:
enable-cache: true
cache-dependency-glob: pyproject.toml

- name: Verify clean checkout has no generated package resources
- name: Verify clean checkout package resource ownership
shell: python
run: |
from pathlib import Path

generated = (
Path("fsq_agent/adapters/control_plane/static"),
Path("fsq_agent/resources"),
)
present = [str(path) for path in generated if path.exists()]
if present:
raise SystemExit(f"Generated package resources are unexpectedly tracked: {present}")

package_resources = (
Path("fsq_agent/config/config.android.yaml"),
Path("fsq_agent/config/config.web.yaml"),
Path("fsq_agent/config/config.windows.yaml"),
Path("fsq_agent/config/config.macos.yaml"),
Path("fsq_agent/config/config.example.yaml"),
Path("fsq_agent/resources/skills/android-harness.md"),
Path("fsq_agent/resources/skills/web-harness.md"),
Path("fsq_agent/resources/skills/windows-harness.md"),
Path("fsq_agent/resources/skills/macos-harness.md"),
Path("fsq_agent/resources/skills/automation-basics.md"),
)
missing = [str(path) for path in package_resources if not path.is_file()]
if missing:
raise SystemExit(f"Tracked package resources are missing: {missing}")

- name: Install test dependencies
run: uv sync --all-extras --reinstall-package fsq-agent

Expand Down Expand Up @@ -231,15 +261,16 @@ jobs:
required_files = (
"fsq_agent/adapters/control_plane/static/control-plane/index.html",
"fsq_agent/adapters/control_plane/static/entry-assets.json",
"fsq_agent/resources/config.android.yaml",
"fsq_agent/resources/config.web.yaml",
"fsq_agent/resources/config.windows.yaml",
"fsq_agent/resources/config.macos.yaml",
"fsq_agent/resources/knowledge/skills/android-harness.md",
"fsq_agent/resources/knowledge/skills/web-harness.md",
"fsq_agent/resources/knowledge/skills/windows-harness.md",
"fsq_agent/resources/knowledge/skills/macos-harness.md",
"fsq_agent/resources/knowledge/skills/automation-basics.md",
"fsq_agent/config/config.android.yaml",
"fsq_agent/config/config.web.yaml",
"fsq_agent/config/config.windows.yaml",
"fsq_agent/config/config.macos.yaml",
"fsq_agent/config/config.example.yaml",
"fsq_agent/resources/skills/android-harness.md",
"fsq_agent/resources/skills/web-harness.md",
"fsq_agent/resources/skills/windows-harness.md",
"fsq_agent/resources/skills/macos-harness.md",
"fsq_agent/resources/skills/automation-basics.md",
"fsq_agent/agent/templates/agent_instructions.j2",
"fsq_agent/agent/templates/task_input.j2",
)
Expand All @@ -253,29 +284,55 @@ jobs:

with ZipFile(rebuilt_wheels[0]) as archive:
rebuilt_names = set(archive.namelist())
runtime_prefixes = (
"fsq_agent/config/config.",
"fsq_agent/resources/skills/",
*required_prefixes,
)
required_runtime_names = {
name for name in names if name.startswith(("fsq_agent/resources/", *required_prefixes))
name for name in names if name.startswith(runtime_prefixes)
}
rebuilt_runtime_names = {
name for name in rebuilt_names if name.startswith(("fsq_agent/resources/", *required_prefixes))
name for name in rebuilt_names if name.startswith(runtime_prefixes)
}
if rebuilt_runtime_names != required_runtime_names:
raise SystemExit("Wheel rebuilt from sdist has different runtime package resources")

retired_wheel_resources = {
name
for name in names
if name.startswith("fsq_agent/resources/knowledge/")
or name
in {
"fsq_agent/resources/config.android.yaml",
"fsq_agent/resources/config.web.yaml",
"fsq_agent/resources/config.windows.yaml",
"fsq_agent/resources/config.macos.yaml",
}
}
if retired_wheel_resources:
raise SystemExit(f"Wheel contains retired package resource: {sorted(retired_wheel_resources)}")

with tarfile.open(sdists[0], "r:gz") as archive:
sdist_names = {name.split("/", 1)[-1] for name in archive.getnames()}
for path in (
"config.android.yaml",
"config.web.yaml",
"config.windows.yaml",
"config.macos.yaml",
"knowledge/skills/automation-basics.md",
):
if path not in sdist_names:
raise SystemExit(f"Sdist is missing build input {path}")
for path in required_files:
if path not in sdist_names:
raise SystemExit(f"Sdist is missing {path}")
retired_sdist_resources = {
name
for name in sdist_names
if name.startswith("knowledge/skills/")
or name
in {
"config.android.yaml",
"config.web.yaml",
"config.windows.yaml",
"config.macos.yaml",
"config.example.yaml",
}
}
if retired_sdist_resources:
raise SystemExit(f"Sdist contains retired root resource: {sorted(retired_sdist_resources)}")
for prefix in required_prefixes:
for suffix in (".js", ".css"):
if not any(name.startswith(prefix) and name.endswith(suffix) for name in sdist_names):
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,23 @@ jobs:
"$RUNNER_TEMP/fsq-release-smoke/bin/fsq" --help
"$RUNNER_TEMP/fsq-release-smoke/bin/fsq-agent" --help
"$RUNNER_TEMP/fsq-release-smoke/bin/python" - <<'PY'
from pathlib import Path
from tempfile import TemporaryDirectory

from fsq_agent.adapters.control_plane import ControlPlaneServer, ControlPlaneServerOptions
from fsq_agent.config import PLATFORM_CONFIG_PATHS, load_platform_settings

with TemporaryDirectory() as temp_dir:
temp_root = Path(temp_dir)
for platform, preset_path in PLATFORM_CONFIG_PATHS.items():
assert preset_path.is_file()
settings = load_platform_settings(
platform,
workspace=temp_root / platform,
user_config_root=temp_root / "user",
)
skills = settings.agent_context.knowledge.skills
assert all(item.path is not None and (skills.dir / item.path).is_file() for item in skills.items)

server = ControlPlaneServer(ControlPlaneServerOptions(open_browser=False))
status, body, content_type = server.static_response("/")
Expand Down Expand Up @@ -114,6 +130,9 @@ jobs:
run: |
$venv = Join-Path $env:RUNNER_TEMP "fsq-install-smoke"
python -m venv $venv
if ($LASTEXITCODE -ne 0) {
throw "Virtual environment creation failed with exit code $LASTEXITCODE."
}
if ($IsWindows) {
$python = Join-Path $venv "Scripts/python.exe"
$fsq = Join-Path $venv "Scripts/fsq.exe"
Expand All @@ -128,10 +147,47 @@ jobs:
throw "Expected exactly one wheel in dist/, found $($wheels.Count)."
}
& $python -m pip install --upgrade pip
if ($LASTEXITCODE -ne 0) {
throw "pip upgrade failed with exit code $LASTEXITCODE."
}
& $python -m pip install $wheels[0].FullName
if ($LASTEXITCODE -ne 0) {
throw "Wheel installation failed with exit code $LASTEXITCODE."
}
& $python -c "import fsq_agent"
if ($LASTEXITCODE -ne 0) {
throw "Installed package import failed with exit code $LASTEXITCODE."
}
& $fsq --help
if ($LASTEXITCODE -ne 0) {
throw "fsq console-script smoke failed with exit code $LASTEXITCODE."
}
& $fsqAgent --help
if ($LASTEXITCODE -ne 0) {
throw "fsq-agent console-script smoke failed with exit code $LASTEXITCODE."
}
$resourceSmoke = @'
from pathlib import Path
from tempfile import TemporaryDirectory

from fsq_agent.config import PLATFORM_CONFIG_PATHS, load_platform_settings

with TemporaryDirectory() as temp_dir:
temp_root = Path(temp_dir)
for platform, preset_path in PLATFORM_CONFIG_PATHS.items():
assert preset_path.is_file()
settings = load_platform_settings(
platform,
workspace=temp_root / platform,
user_config_root=temp_root / "user",
)
skills = settings.agent_context.knowledge.skills
assert all(item.path is not None and (skills.dir / item.path).is_file() for item in skills.items)
'@
$resourceSmoke | & $python -
if ($LASTEXITCODE -ne 0) {
throw "Installed package resource smoke failed with exit code $LASTEXITCODE."
}

publish:
name: Publish to PyPI
Expand Down
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ screenshots/
dist/
*.egg-info/
knowledge/*
!knowledge/skills/
!knowledge/skills/**
node_modules/
.frontend-dist/
fsq_agent/adapters/control_plane/playground/static/
fsq_agent/adapters/control_plane/static/
fsq_agent/resources/
fsq_agent/resources/*
!fsq_agent/resources/skills/
!fsq_agent/resources/skills/**


# Private/local planning material. Do not commit.
Expand Down
3 changes: 2 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Existing-Case testing parses the Case through FSQ rather than treating YAML as u

Except while creating an unregistered Workspace, the exact CLI current directory is a registered workspace root using the canonical `.fsq/config/config.<platform>.yaml`, `.fsq/runs/<platform>/`, `cases/<platform>/`, and `knowledge/<platform>/` layout. CLI does not create or accept `.fsq-agent-workspace` markers, search parents, or auto-initialize. For a new name, `fsq init` treats the current directory as the selected directory: an empty directory becomes the Workspace root, while a non-empty directory receives a new `<selected-directory>/<workspace-name>` child. For an existing registered name, initialization uses its stored root independently of the process current directory. All other CLI commands require the exact registered root, and platform operations require the selected platform. Control Plane uses the same Application and Config-owned root-selection and registry rules while retaining explicit browser workspace selection independent of its startup directory.

Default local LLM runs use GitHub Copilot provider authentication with Copilot model `gpt-5.5` and tracing enabled. Provider selection and credentials are managed by the Provider configuration surface rather than workspace initialization. Repository-owned platform YAML presets are committed as `config.android.yaml`, `config.web.yaml`, `config.windows.yaml`, and `config.macos.yaml`; `config.example.yaml` is reference-only. Workspace platform configuration owns local target identity and private runtime-secret values. A Web target always names a browser channel and may omit its executable path so Application can discover exactly one compatible host executable before Driver readiness or workspace mutation.
Default local LLM runs use GitHub Copilot provider authentication with Copilot model `gpt-5.5` and tracing enabled. Provider selection and credentials are managed by the Provider configuration surface rather than workspace initialization. Repository-owned platform YAML presets are package-owned files under `fsq_agent/config/`; the sibling `config.example.yaml` is reference-only. Reusable preset skills are tracked package resources under `fsq_agent/resources/skills/`. Source checkouts and installed distributions resolve the same package-owned preset and skill files. Workspace platform configuration owns local target identity and private runtime-secret values. A Web target always names a browser channel and may omit its executable path so Application can discover exactly one compatible host executable before Driver readiness or workspace mutation.

The local workspace setup entry is `fsq init --platform android|web|windows|macos` with the selected platform's target options and optional `--name`. It creates an unregistered Workspace from the current selected directory or initializes and updates exactly one platform at the stored root of an existing registered name. It does not configure Providers or create legacy workspace markers.

Expand Down Expand Up @@ -161,6 +161,7 @@ Loader diagnostics such as missing optional skills or missing optional knowledge
- New frontend application modules use Vite, React, and TypeScript/TSX unless their confirmed module SPEC records a concrete exception.
- `ts-ebml` is an exact npm dependency consumed through an ES module import. Third-party browser bundles and Vite-generated assets are not tracked in Git.
- Vite-generated Control Plane assets live under `fsq_agent/adapters/control_plane/static`. Its HTML entry point, JavaScript, CSS, entry-asset manifest, and referenced generated assets are included in both wheel and source distribution. Release builds run the npm build before Python distribution construction. An installed distribution is self-contained and does not require Node.js or network access to serve the frontend at runtime.
- The npm build generates and distributes frontend assets only; it does not generate, copy, delete, or mutate tracked Python platform presets or reusable skill resources.
- Frontend development may use the Vite development server with API and streaming requests proxied to the Control Plane Python server. Production and installed-wheel usage serve the generated entry and its APIs from one Python process.

## Architecture Diagram
Expand Down
Loading
Loading