{"owner":"semaphoreui","repo":"semaphore","hasSkills":true,"totalSkillsCount":15,"totalTokensCount":19569,"categories":["claude-rule","anthropic-skill","copilot-instructions","root-instruction","subagent-persona"],"hasMcp":false,"mcpConfig":null,"found":[".claude/CLAUDE.md",".claude/skills/semaphore-checkout/SKILL.md",".claude/skills/semaphore-cli-docs/SKILL.md",".claude/skills/semaphore-commit/SKILL.md",".claude/skills/semaphore-config-schema/SKILL.md",".claude/skills/semaphore-third-party-licenses/SKILL.md",".claude/skills/semaphore-third-party-licenses/references/license_policy.md",".claude/skills/semaphore-third-party-licenses/references/template.md",".github/copilot-instructions.md","AGENTS.md","AGENTS/agents/setup/db-query/README.md","AGENTS/docs/INDEX.md","AGENTS/tools/wf-stand/README.md","AGENTS/work/workflow-editor/mocks/README.md","CLAUDE.md"],"skills":{".claude/CLAUDE.md":"# Claude Code Instructions\n\n# Documentation\n\nThe documentation stored in /docs directory. It is a submodule with official Semaphore UI documentation\nin on Docusaurus engine.\n\nYou should actualize the documentation for new and updated featues.\n\n### Generated reference pages\n\n`docs/docs/reference/configuration.md` and `docs/docs/reference/cli/commands.md` are generated\nfrom this repository and must never be edited by hand:\n\n- `tools/docsref` reads the struct tags and doc comments of `util.ConfigType`.\n- `tools/clidocs` walks the real Cobra command tree from `cli/cmd`.\n\nAfter changing a configuration option or a CLI command, run `task docs:gen` and commit\nthe result together with the code. The `docs-reference` job in `.github/workflows/dev.yml`\nfails when the committed pages differ from what the generators produce.\n\nA config field with no doc comment falls back to `tools/docsref/descriptions.json`.\nPrefer writing the doc comment: `docsref` prints how many options still depend on that\nfile, and the number should only fall.\n\n## Writing Plans\n\nAll plans, tasks, researches for AI agents stored in folder AGENTS.\n\nEach plan has markdown-format and stored in folder AGENTS/plans/<version>.\n\nPlan can be split to tasks. Each task describes in details how to implement some part of some plan.\n\n## Code Style\n\n1. Do not use global variables. Global variables are forbidden.\n\n## High Availability Support\n\nAll solutions must work in High Availability mode.\n\n## Security Is the #1 Priority\n\nAll solutions must be secure by design. Do not consider any solution that introduces security risks.\n\n## How to do research\n\nUse MCP server `research` if your asks you to research something.\n\n## Writing Tests\n\nUse `github.com/stretchr/testify/assert` (and `require` when a failure should stop the test immediately).\n\n### Run tests\n\n```bash\ngo test ./path/to/package/ -run TestFunctionName -v -count=1\n```\n\n### Rules\n\n- Test file goes next to the source: `foo.go` → `foo_test.go`, same package.\n- Use `assert.Equal`, `assert.Empty`, `assert.Nil`, `assert.True`, `assert.Panics`, etc. Never use raw `if`/`t.Fatalf` for assertions.\n- Use `require` (instead of `assert`) when subsequent lines depend on the check passing (e.g., `require.NoError` before using a result).\n- Use table-driven tests with `t.Run` for multiple inputs to the same function.\n- Use `t.TempDir()` for temporary files — cleanup is automatic.\n- When testing code that uses package-level globals (e.g., `util.Config`), initialize them in a helper and reset state between tests.\n- When testing code that reads `os.Stdin`, swap it with `os.Pipe()` and restore in `defer`.\n\n### Template\n\n```go\npackage mypkg\n\nimport (\n    \"testing\"\n\n    \"github.com/stretchr/testify/assert\"\n    \"github.com/stretchr/testify/require\"\n)\n\nfunc TestMyFunction(t *testing.T) {\n    // setup\n    input := \"value\"\n\n    // act\n    result, err := MyFunction(input)\n\n    // assert\n    require.NoError(t, err)\n    assert.Equal(t, \"expected\", result)\n}\n\nfunc TestMyFunction_TableDriven(t *testing.T) {\n    tests := []struct {\n        name     string\n        input    string\n        expected string\n    }{\n        {\"short input\", \"abc\", \"ABC\"},\n        {\"empty input\", \"\", \"\"},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            assert.Equal(t, tt.expected, MyFunction(tt.input))\n        })\n    }\n}\n\nfunc TestMyFunction_Panics(t *testing.T) {\n    assert.Panics(t, func() {\n        MyFunction(nil)\n    })\n}\n```\n\n### Common assertions\n\n| Assertion | Use when |\n|-----------|----------|\n| `assert.Equal(t, expected, actual)` | Comparing values |\n| `assert.NotEqual(t, a, b)` | Values must differ |\n| `assert.Nil(t, val)` / `assert.NotNil(t, val)` | Pointer/interface checks |\n| `assert.Empty(t, val)` | String, slice, or map is zero-length |\n| `assert.Contains(t, str, substr)` | Substring or element in collection |\n| `assert.True(t, cond)` / `assert.False(t, cond)` | Boolean conditions |\n| `assert.NoError(t, err)` / `assert.Error(t, err)` | Error checks |\n| `assert.ErrorContains(t, err, \"msg\")` | Error with specific message |\n| `assert.Panics(t, func(){...})` | Code must panic |\n| `require.NoError(t, err)` | Stop test immediately on error |\n\n### Initializing `util.Config` in tests\n\nMany packages depend on `util.Config` which is a `*ConfigType` pointer (nil by default). Initialize before use:\n\n```go\nfunc setupConfig() {\n    if util.Config == nil {\n        util.Config = &util.ConfigType{}\n    }\n    // Initialize nested pointers as needed:\n    if util.Config.Runner == nil {\n        util.Config.Runner = &util.RunnerConfig{}\n    }\n}\n```\n\n### HTTP handler tests\n\nUse `net/http/httptest`:\n\n```go\nfunc TestMyHandler(t *testing.T) {\n    req := httptest.NewRequest(http.MethodGet, \"/api/endpoint\", nil)\n    w := httptest.NewRecorder()\n\n    myHandler(w, req)\n\n    assert.Equal(t, http.StatusOK, w.Code)\n    assert.Contains(t, w.Body.String(), \"expected\")\n}\n```\n",".claude/skills/semaphore-checkout/SKILL.md":"---\nname: semaphore-checkout\ndescription: >-\n    Create and switch to a branch with the same name in both repos — the root repo and pro_impl. \n    Usage: /semaphore-checkout <branch_name>\n---\n\nCreate a branch named `<branch_name>` in both repositories at once — the root repo\n(current directory) and the PRO module repo (`./pro_impl`) — and switch both to it.\n\nCI depends on branch names matching: GitHub Actions clones the `pro_impl` branch\nwith the same name as the root repo branch and falls back to `main` if it does not\nexist. This skill keeps the two repos in sync.\n\n## Arguments\n\n- `<branch_name>` (required) — the branch to create. If not provided, ask the user\n  for it. Validate it with `git check-ref-format --branch <branch_name>` before use.\n\n## Behavior\n\nFor each repo — root (`.`) first, then `./pro_impl`:\n\n1. Check the working tree is clean (`git status --porcelain`). If there are\n   uncommitted changes in either repo, stop and report which repo is dirty —\n   do not stash or discard anything.\n2. Fetch the latest state: `git fetch origin`.\n3. Create the branch from the up-to-date default branch and switch to it:\n   - root repo: `git checkout -b <branch_name> origin/develop`\n   - pro_impl: `git checkout -b <branch_name> origin/main`\n4. If the branch already exists locally, just `git checkout <branch_name>`\n   instead of creating it. If it exists only on origin, check it out tracking\n   the remote branch.\n\nDo not push the new branches — they are pushed later with the first commit.\n\n## Rules\n\n- Never force-create (`-B`) or reset an existing branch.\n- Both repos must end up on the same branch name; if step for `pro_impl` fails\n  after the root repo already switched, report the inconsistent state clearly.\n- Report the final state at the end: branch name and start point in each repo.\n\n## Example\n\n```\n/semaphore-checkout fix/task-env-vars\n```\n\nResult:\n\n```\nRoot repo:  fix/task-env-vars (from origin/develop)\npro_impl:   fix/task-env-vars (from origin/main)\n```\n",".claude/skills/semaphore-cli-docs/SKILL.md":"---\nname: semaphore-cli-docs\ndescription: >\n  Generate or update the CLI documentation under `docs/docs/reference/cli/` from the Cobra command code in\n  `cli/`. Use this skill whenever the user asks to \"update the CLI docs\", \"actualize/sync the CLI documentation\",\n  \"document the CLI\", \"the CLI docs are out of date\", \"regenerate the runner/user/vault/project/migrate docs\", or\n  after adding or changing a Cobra command, subcommand, flag, alias, or required-argument check anywhere under\n  `cli/cmd/`. Covers every command group (`users`, `projects`, `vaults`, `runner`, `migrate`) plus the operational\n  commands (`server`, `setup`, `version`) and the global flags on the root command. Trigger when the user adds a\n  new `*Cmd` Cobra command, a new `PersistentFlags()` call, or renames/removes a CLI flag.\n---\n\n# Semaphore — CLI Documentation Generator\n\nThis skill produces and maintains the CLI reference pages under\n`docs/docs/reference/cli/` and the CLI index `docs/docs/reference/cli/README.md`. Every page is **derived from the\nCobra command code** in `cli/cmd/`. If a command, subcommand, or flag isn't in the code, it must not be in the\ndocs; if a flag's name, default, or required-ness changed, the docs must change with it.\n\n> `docs/` is a **git submodule** (the documentation site). Editing files under `docs/docs/...` and\n> `docs/sidebars.js` is correct; those changes are committed to the submodule, not the main repo.\n\nThe docs are the human-facing contract. The Go code is the source of truth. When they disagree, the code wins —\n**but** read the command's `Run` body, not just its help strings, because help text drifts (see Pitfalls).\n\n> **`docs/docs/reference/cli/commands.md` is not yours.** It is generated by `tools/clidocs`, which walks the real\n> Cobra tree and emits every command and flag with its Cobra help string. Run `task docs:gen` for it and never edit\n> it by hand. This skill owns the *task-oriented* pages next to it: what each command group is for, what the\n> arguments mean in practice, what the command actually does to the database, and worked examples. Do not try to\n> reproduce an exhaustive flag inventory here — link to `/reference/cli/commands` instead and spend the words on\n> behaviour the help strings cannot express.\n\n## Inputs (source of truth)\n\nRead these in full before writing. Each command group maps to exactly one page.\n\n| Source files (`cli/cmd/`) | Doc page |\n|---------------------------|----------|\n| `root.go` | `docs/docs/reference/cli/README.md` — global flags, command overview |\n| `server.go`, `setup.go`, `version.go` | `docs/docs/reference/cli/README.md` — operational commands |\n| `user.go`, `user_add.go`, `user_change.go`, `user_delete.go`, `user_get.go`, `user_list.go`, `user_token.go`, `user_totp.go` | `docs/docs/reference/cli/users.md` |\n| `project.go`, `project_export.go`, `project_import.go` | `docs/docs/reference/cli/projects.md` |\n| `vault.go`, `vault_check.go`, `vault_rekey.go` | `docs/docs/reference/cli/vaults.md` |\n| `runner.go`, `runner_register.go`, `runner_setup.go`, `runner_start.go`, `runner_unregister.go` | `docs/docs/reference/cli/runners.md` |\n| `migrate.go` | `docs/docs/reference/cli/migrations.md` |\n\n`*_test.go` files (e.g. `runner_register_test.go`, `user_token_test.go`) encode exact behavior — read them to\ndocument precise rules (e.g. registration-token resolution order, token expiry formatting).\n\n`syslog.go` / `syslog_windows.go` are internal helpers, **not** commands — never document them.\n\nRe-discover the command tree before each run; don't trust this table if files were added/removed:\n\n```bash\nls cli/cmd/*.go | grep -v _test\ngrep -rn \"cobra.Command{\" cli/cmd/ | grep -v _test          # every command/subcommand\ngrep -rn \"AddCommand\\|PersistentFlags()\\.\" cli/cmd/          # wiring + flags\n```\n\n## How to read a Cobra command\n\nExtract these facts mechanically from each `*Cmd = &cobra.Command{...}` and its `init()`:\n\n- **Command name** — the `Use:` field (first word). Subcommands are wired with `parent.AddCommand(child)`.\n- **Aliases** — the `Aliases:` slice. Document them (e.g. `users`/`user`, `projects`/`project`,\n  `vaults`/`vault`, `server`/`service`). A command whose `Run` only calls `cmd.Help()` is a pure group — it has\n  no behavior of its own, just subcommands.\n- **Flags** — each `cmd.PersistentFlags().<Type>Var(&arg, \"name\", default, \"usage\")` becomes one table row:\n  flag = `--name`, description from the usage string, and note the default when it's meaningful (e.g.\n  `--enabled` defaults to `true`, `--project-id` defaults to `0` = \"global\"). `StringSliceVar` → \"comma-separated\n  or repeat the flag\".\n- **Required / mutually-exclusive args** — read the `Run` body. Patterns like\n  `if args.x == \"\" { fmt.Println(\"Argument --x required\"); ok = false }` mean **required**; checks that reject\n  setting both of two flags mean **exactly one of**. Document these rules explicitly (\"Exactly one of `--a` or\n  `--b` is required\").\n- **Output** — what the command prints on success (IDs, a table, nothing). Mirror the real `fmt.Printf` format\n  in the doc, including example output blocks for commands that print structured results (e.g. `vault check`).\n\n### Global flags (from `root.go`)\n\nDocumented once in `cli.md`; they apply to every command. Currently: `--config`, `--no-config`, `--log-level`\n(`DEBUG|INFO|WARN|ERROR|FATAL|PANIC`, env `SEMAPHORE_LOG_LEVEL`), `--debug-filter` (DEBUG-only namespace filter,\nenv `SEMAPHORE_DEBUG_FILTER`). Re-read `Execute()` in `root.go` for the live list.\n\n## Page structure (match the existing style)\n\nEvery command-group page follows the shape established in `users.md` and `vaults.md`:\n\n1. `# <Title>` then a one-sentence purpose.\n2. A fenced `semaphore <group> --help` block.\n3. An alias note as a `>` blockquote when the group has an alias (`> \\`vault\\` is an alias for \\`vaults\\`.`).\n4. A subcommand table (`| Command | Purpose |`) with in-page anchor links when the page is long.\n5. One `##` section per subcommand: a short description, a copy-pasteable example, then a\n   `| Flag | Description |` table. State required/exactly-one rules in prose under the table.\n\nConventions:\n\n- Internal links are absolute from the route base `/`: **`/reference/cli/<page>`** (NOT `/cli/<page>` — that\n  base is wrong and 404s). Cross-links to concept pages use the same form, e.g. `/admin-guide/runners`,\n  `/admin-guide/security/encryption`.\n- Admonitions use Docusaurus syntax: `:::tip`, `:::warning` … `:::`.\n- Keep prose task-oriented and match the tone/heading depth of neighboring pages.\n\n## Index page (`cli.md`) and sidebar\n\n- `cli.md` carries: intro + `semaphore help`, a command-group table linking to each page, the **global options**\n  table, and short sections for `version`, `setup`, `server`, `runner`, `migrate`.\n- Every command-group page must be registered in **`docs/sidebars.js`** under the `label: 'CLI'` category's\n  `items` array, as `'reference/cli/<page>'`. Adding a page without registering it hides it from navigation.\n\n## Workflow\n\n### 1. Enumerate the live command tree\nRun the discovery greps above. Build the set of command groups and subcommands that actually exist. Note any\ngroup/subcommand/flag that has no corresponding doc section, and any doc section with no corresponding code.\n\n### 2. Extract facts per command\nFor each command, pull name, aliases, flags (name/default/usage), required/exclusive rules (from `Run`), and\nsuccess output. Cross-check `*_test.go` for exact behavior.\n\n### 3. Update each page\nRewrite or patch each page so it matches the extracted facts and the page structure above. Add sections for new\nsubcommands; remove sections for deleted ones; fix flag tables. Leave a page untouched only after confirming it\nalready matches the code.\n\n### 4. Update the index and sidebar\nRefresh `cli.md` (command table, global options, operational commands) and ensure every page is listed in\n`docs/sidebars.js`.\n\n### 5. Build to catch broken links\nFrom the `docs/` directory:\n\n```bash\ncd docs && npm run build 2>&1 | tail -40\n```\n\nThe build must end with `[SUCCESS] Generated static files`. **Broken-link** errors in pages you touched must be\nfixed. Pre-existing **broken-anchor** warnings in unrelated pages (e.g. `admin-guide/configuration`,\n`admin-guide/installation_manually`) are not yours to fix — note them and move on. If `node_modules` is absent,\nrun `npm install` first (or tell the user the build couldn't run).\n\n## Pitfalls\n\n- **Dead flags.** A flag registered in `init()` but never read in the command's `Run` does nothing — do not\n  document it as functional. Example: after BoltDB removal, `migrate` still registers `--err-log-size`,\n  `--skip-task-output`, `--merge-existing-users`, but `Run` only uses `--apply-to` / `--undo-to`. Verify each\n  flag is actually consumed before documenting it.\n- **Removed features → version-gate, don't silently delete.** When a command/flag is removed (e.g.\n  `migrate --from-boltdb`; BoltDB is \"not supported starting from 2.19\" per `cli/setup/setup.go`), keep the\n  guidance but mark the version boundary with a `:::warning`, so users on older versions are still served. Note\n  when other parts of the repo (e.g. `deployment/docker/server/server-wrapper`) still reference a removed flag —\n  that's a stale call site worth flagging to the user, but out of scope for the docs change.\n- **Stale help strings.** A command's `Short`/`Long` text can lag the code. Verify config-key names against the\n  actual structs in `util/` (e.g. `vault rekey`'s `--old-key` help says `access_key`, but the real config field\n  is `secret_key` / `secret_key_file` in `util/config.go`). Document the struct, not the help string.\n- **Group commands have no flags of their own.** `users`, `projects`, `vaults`, `runner`, `token`, `totp` just\n  print help and dispatch to subcommands. Don't invent flags for them.\n- **Don't document the internal `server` runtime.** `server.go`/`root.go` wire a lot of services; the doc only\n  needs \"start the server, `service` is an alias, respects global flags.\"\n\n## When to ask or refuse\n\n- If a command's behavior is genuinely ambiguous from the code (e.g. a flag is read but its effect spans\n  packages you can't quickly trace), summarize what you found and ask the user rather than guessing.\n- If asked to document a flag or command that **doesn't exist in `cli/cmd/`**, refuse and explain: CLI docs are\n  generated from the Cobra code — add the command/flag first, then regenerate.\n- Before deleting a whole documented section (a removed command group), propose it explicitly; prefer\n  version-gating over silent removal when older releases still ship the feature.\n",".claude/skills/semaphore-commit/SKILL.md":"---\nname: semaphore-commit\n---\n\nCreate well-formatted git commits following conventional commit standards.\n\n## Behavior\n1. Analyze staged changes with `git diff --staged`\n2. Generate a conventional commit message\n3. Create the commit with proper formatting\n4. Do not add \"Co-Authored-By: Claude ... <noreply@anthropic.com> to the footer\"\n\n## Commit Format\n```\n<type>(<scope>): <description>\n\n[optional body]\n\n[optional footer]\n```\n\n## Types\n- feat: New feature\n- fix: Bug fix\n- docs: Documentation changes\n- style: Code style changes\n- refactor: Code refactoring\n- test: Adding or modifying tests\n- chore: Maintenance tasks\n- agents: Changes related to AI agents work\n\n## Example Output\n```\nfeat(auth): add password reset functionality\n\n- Add forgot password form\n- Implement email verification flow\n- Add password reset endpoint\n```\n",".claude/skills/semaphore-config-schema/SKILL.md":"---\nname: semaphore-config-schema\ndescription: >\n  Generate or update the JSON Schema (in YAML form) that describes the Semaphore configuration file. The schema\n  lives at `config.schema.yaml` in the repository root and is derived from the Go struct `util.ConfigType`\n  (with nested types in `util/config.go`, `util/config_auth.go`, `util/App.go`, `util/OdbcProvider.go`). Use this\n  skill whenever the user asks to \"regenerate the config schema\", \"update config.schema.yaml\", \"sync the schema\n  with ConfigType\", \"add a new field to the schema\", \"the schema is out of date\", or after a Go struct change\n  that affects configuration. Also trigger when the user adds a new top-level field, new nested struct, or\n  changes a `json:`, `default:`, `rule:`, or `env:` tag on a config struct field.\n---\n\n# Semaphore — Config Schema Generator\n\nThis skill produces and maintains `config.schema.yaml` — a **JSON Schema draft 2020-12** document (written in\nYAML) describing every valid key in a Semaphore `config.json` / `config.yaml`. The schema is the source of truth\nfor IDE autocomplete (via `yaml-language-server`) and for any future server-side config validation.\n\nThe schema is **derived from Go code**, never hand-edited speculatively. If `ConfigType` doesn't have a field,\nthe schema must not have it. If a field has a `default:` or `rule:` tag, the schema must reflect it.\n\n## Inputs (source of truth)\n\nAll Go types live in the `util` package. Read these files **in full** before writing anything:\n\n| File | What's in it |\n|------|--------------|\n| `util/config.go` | `ConfigType` (root) + most nested types: `DbConfig`, `TLSConfig`, `RunnerConfig`, `TotpConfig`, `EventLogType`, `TaskLogType`, `ConfigLog`, `SyslogConfig`, `ConfigProcess`, `ConfigAppNamespaces`, `ScheduleConfig`, `DebuggingConfig`, `HAConfig`, `HARedisConfig`, `TeamsConfig`, `ConfigDirs`, `SubscriptionConfig`, `LdapMappings`, `oidcEndpoint` |\n| `util/config_auth.go` | `MultifactorAuthConfig`, `EmailAuthConfig`, `TotpConfig`, `RecaptchaConfig` |\n| `util/App.go` | `App` |\n| `util/OdbcProvider.go` | `OidcProvider` (note: filename is `Odbc...`, typo'd — don't \"fix\" it) |\n\nIf a struct moves or a new file appears, find it with:\n\n```bash\ngrep -rn \"^type .* struct\" util/ | grep -v _test\n```\n\n## Output\n\nA single file: **`config.schema.yaml`** in the repository root.\n\nHeader (top of file) — keep exactly this shape:\n\n```yaml\n$schema: \"https://json-schema.org/draft/2020-12/schema\"\n$id: \"https://semaphoreui.com/schemas/config.schema.json\"\ntitle: Semaphore configuration\ndescription: Schema for Semaphore UI config.json / config.yaml. Generated from util.ConfigType.\ntype: object\n```\n\n## Field-mapping rules\n\nThese are non-negotiable — apply them mechanically.\n\n### Property name\n\nUse the **`json:` tag's first segment** (before any comma):\n\n```go\nPort string `json:\"port,omitempty\" ...`   // → property name \"port\"\n```\n\nSkip fields where the `json:` tag is `\"-\"` (e.g. `RunnerConfig.RegistrationToken`, `SubscriptionConfig.Key` —\nthose are env-only, not file-configurable). Document this in the schema's `description` for the containing\nstruct if a sensitive token is reachable only via env.\n\nSkip unexported fields (lowercase first letter) — but include their **referenced types** if they appear via\nexported fields. Example: `oidcEndpoint` is unexported but referenced from `OidcProvider.Endpoint`, so define\nit under `$defs` as `OidcEndpoint`.\n\n### JSON Schema `type`\n\n| Go type | Schema type |\n|---------|-------------|\n| `string` | `string` |\n| `bool` | `boolean` |\n| `int`, `int64`, `uint32` | `integer` |\n| `float64` | `number` |\n| `[]T` | `array`, with `items: { ... }` |\n| `map[string]T` | `object`, with `additionalProperties: { ... }` |\n| `*T` (pointer) | same as `T`. If `*int`, allow `null`: `type: [integer, \"null\"]` |\n| `*Struct` | `{ $ref: \"#/$defs/Struct\" }` |\n\n### Constraints from tags\n\n- **`default:\"X\"`** → emit `default: X` (cast to the right type — `default:\"10\"` on `int` becomes `default: 10`).\n- **`rule:\"^...$\"`** → emit `pattern: \"^...$\"`. **Don't** also add a `format:` — the regex is more authoritative.\n- **`env:\"NAME,sensitive\"`** → append `(sensitive).` to the `description`. The schema doesn't have a native\n  \"sensitive\" flag, so this is documentation-only.\n- **`env:\"NAME\"`** without `sensitive` → no change to schema.\n- URL-looking string fields (web hooks, redirect URLs) → add `format: uri` **unless** a `pattern` is already\n  set from a `rule:` tag.\n\n### `enum` discovery\n\n`enum` is not always in tags — sometimes it's in code:\n\n1. Check for a `rule:\"^a|b|c$\"` regex — convert that to `enum: [a, b, c]` instead of a pattern (more useful in\n   IDE).\n2. Check for typed constants near the struct (e.g. `SyslogFormat` has `SyslogDefault = \"\"` and `SyslogRFC5424\n   = \"rfc5424\"` → `enum: [\"\", rfc5424]`).\n3. `TeamInviteType` constants (`TeamInviteEmail`, `TeamInviteUsername`, `TeamInviteBoth`) → enum.\n\n### Reusable types → `$defs`\n\nEvery named struct that appears as a field type gets a `$defs/<TypeName>` entry. Reference via `$ref:\n\"#/$defs/<TypeName>\"`. Top-level `properties` should reference, not inline.\n\nException: `lumberjack.Logger` is external — define a minimal stub `$defs/LumberjackLogger` with the common\nfields (`filename`, `maxsize`, `maxage`, `maxbackups`, `localtime`, `compress`). Do not try to fully model it.\n\n### `additionalProperties`\n\nLeave it **unset** (defaults to `true`). The schema is informational, not strict — making it strict would\nbreak existing configs that have stray keys (legacy fields, typos used as comments, etc.).\n\n## Workflow\n\n### 1. Read all four Go files\n\nUse `Read` on each file. Do not skim. Note every exported field on every struct.\n\n### 2. Build a mental map: struct → properties\n\nFor `ConfigType` (the root), list every exported field with its `json:` name, Go type, and any `default:` /\n`rule:` / `env:` tags. Then for each referenced struct, do the same. Stop when no new structs appear.\n\n### 3. Diff against existing `config.schema.yaml`\n\nBefore writing, compare what Go has vs. what the schema has:\n\n```bash\n# List all json tag names in Go\ngrep -hoE 'json:\"[^,\"]+' util/*.go | sed 's/json:\"//' | sort -u > /tmp/go-keys.txt\n\n# List all property keys in the schema\nyq '.properties | keys | .[]' config.schema.yaml | sort -u > /tmp/schema-keys.txt\n\ndiff /tmp/go-keys.txt /tmp/schema-keys.txt\n```\n\nIdentify three categories:\n- **In Go, missing from schema** → add.\n- **In schema, missing from Go** → remove (the Go struct is the source of truth).\n- **In both** → check that type / default / pattern still match.\n\nShow the user the diff before applying changes if it's larger than ~5 lines.\n\n### 4. Write `config.schema.yaml`\n\nUse the **YAML formatting conventions** already established in the file:\n\n- Section comments split top-level `properties` into visual groups:\n  `# === Database ===`, `# === HTTP server ===`, `# === Authentication ===`, etc.\n- Inline-object syntax `{ type: string, default: foo }` for one-property entries.\n- Multi-line block form when there are 3+ properties or a `description`.\n- Quote string values that contain `:`, `/`, `+`, `=`, or start with a digit. Don't quote bare identifiers\n  like `string`, `boolean`, `integer`.\n- `$ref` values are quoted: `{ $ref: \"#/$defs/DbConfig\" }`.\n\n### 5. Validate\n\nAfter writing, run two checks:\n\n```bash\npython3 - <<'PY'\nimport yaml, jsonschema\nwith open(\"config.schema.yaml\") as f:\n    schema = yaml.safe_load(f)\njsonschema.Draft202012Validator.check_schema(schema)  # meta-validation\nprint(\"schema is valid\")\n\n# Confirm an existing config.yaml still passes\ntry:\n    with open(\"config.yaml\") as f:\n        cfg = yaml.safe_load(f)\n    errors = list(jsonschema.Draft202012Validator(schema).iter_errors(cfg))\n    print(f\"config.yaml: {len(errors)} error(s)\")\n    for e in errors:\n        print(f\"  {list(e.path)}: {e.message}\")\nexcept FileNotFoundError:\n    pass\nPY\n```\n\nBoth must pass. If `config.yaml` doesn't exist locally, skip the second check — the meta-validation alone is\nsufficient.\n\n### 6. Don't touch `config.yaml`\n\nThe user's `config.yaml` may contain stray legacy keys or comments. Do not \"clean it up\" as part of this\nskill — the schema generation is a one-way operation Go → schema. If the user wants a cleaner config, that's a\nseparate task.\n\n## Common pitfalls\n\n- **`json:\"-\"` fields**: easy to accidentally include. Always check the tag, not the field name.\n- **Pointer vs. value**: `*int` is nullable, plain `int` is not. Reflect that in `type`.\n- **Typo'd filenames**: `util/OdbcProvider.go` (should be `Oidc`) — preserve it; renaming files is out of scope.\n- **Tag inheritance illusion**: nested structs don't inherit tags from their parent field. The `default:` on\n  `ConfigType.MaxParallelTasks` is on the field, not on `int`.\n- **`sensitive` is not standard**: JSON Schema has no built-in sensitive marker. Encode it in `description`\n  text only.\n- **Existing keys with apostrophes** in `config.yaml` (e.g. `\"'email_sender\"`) are user-side comment hacks —\n  ignore them, they're not in any Go struct.\n\n## When to refuse\n\nIf the user asks to add a property to the schema that **doesn't exist in `ConfigType`** (\"just add a `foo`\nfield so my config validates\"), refuse and explain: the schema is generated from Go. The fix is to add the\nfield to the Go struct first, then regenerate.\n\nIf `ConfigType` has been heavily refactored and a previous schema entry no longer maps, propose the deletion\nexplicitly before doing it — never silently drop entries that the user might have been relying on.\n",".claude/skills/semaphore-third-party-licenses/SKILL.md":"---\nname: semaphore-third-party-licenses\ndescription: >\n  Generate or update the THIRD-PARTY-LICENSES.md file for the Semaphore UI repository. Use this skill whenever the user \n  asks to create, update, regenerate, audit, or refresh third-party license attributions, OSS notices, SBOM-style \n  license inventories, or NOTICE files for Semaphore UI. Trigger on phrases like \"third-party licenses\", \"OSS notices\",\n  \"license attribution file\", \"SBOM for licenses\", \"NOTICE file\", \"audit dependencies\",\n  \"licenses for go.mod / package.json\", or any request that mentions compliance with customer agreements requiring an\n  OSS components list. Also trigger when the user says \"we added a new dependency, regenerate licenses\". This skill\n  knows the Semaphore UI layout (Go backend + Vue.js frontend), the project's license policy (allowlist/denylist),\n  and the canonical output format.\n---\n\n# Semaphore UI — Third-Party License Generator\n\nThis skill produces `THIRD-PARTY-LICENSES.md` for the Semaphore UI repository. The file lists every open-source\ndependency shipped with Semaphore UI by **name**, **version**, **license**, and **source URL**, grouped by component. It\nexists to satisfy contractual obligations toward customers (identification of OSS components by name, version,\nand license type) and to preserve attribution required by permissive licenses (MIT/BSD/Apache-2.0).\n\n## Repository layout you can expect\n\nSemaphore UI is a self-hosted product. The repository ships:\n\n- **Go backend** — module file at `go.mod`, source under `pkg/`, `services/`, `db/`, etc.\n- **Vue.js frontend** — `web/package.json` (or `web2/package.json` in some branches), built into static assets and\n  embedded in the Go binary.\n\nExternal CLI tools (Ansible, Terraform, OpenTofu, Pulumi, Bash) are invoked at runtime as separate operating-system\nprocesses. They are NOT linked into the binary and NOT redistributed with Semaphore UI, so they are intentionally\n**not listed** in `THIRD-PARTY-LICENSES.md` — their license terms apply to the user who installs them on the host,\nnot to Semaphore UI's distribution. The license policy still discusses why this is contractually safe (see\n`references/license_policy.md`, \"Subprocess exception\").\n\nIf the layout differs (monorepo restructure, new submodule, etc.), inspect the repo first with\n`find . -name \"go.mod\" -o -name \"package.json\" -not -path \"*/node_modules/*\"` and adapt.\n\n## Workflow\n\nFollow these steps in order. Don't skip the policy check — it's the part that actually protects the project from\naccidentally shipping a GPL'd dependency.\n\n### 1. Confirm scope with the user\n\nBefore generating anything, ask (briefly) which targets to include if it's not obvious from context:\n\n- Backend only? Frontend only? Both?\n- Production dependencies only, or also dev dependencies?\n\n**Default**: both backend and frontend, **production-only** (dev dependencies are not distributed to customers and don't\nneed attribution). Override only if the user explicitly asks for dev deps too.\n\n### 2. Collect raw license data\n\nRun `scripts/collect_licenses.sh` from the repo root. It does the following:\n\n- Sets `GOWORK=off` and runs `go-licenses report ./... --template scripts/go_template.tpl` for the Go backend, so\n  workspace siblings (e.g. `pro_impl`) are not pulled in.\n- Rolls sub-package rows up to their parent module (uses `go list -m all` for the module → version map) and drops\n  any module path that belongs to the root module's namespace (first-party packages, including `replace … => ./pro`\n  targets).\n- Applies a small `OVERRIDES` table in the script for modules where `go-licenses` can't auto-detect a license\n  (e.g. `modernc.org/mathutil` → BSD-3-Clause). Extend the table when new \"Unknown\" entries appear.\n- Runs `license-checker --production --json --excludePrivatePackages` in the frontend directory and strips the\n  low-confidence `*` suffix from license strings so `MIT*` collapses into `MIT`.\n- Writes raw output to `.licenses-cache/go.tsv` and `.licenses-cache/npm.json`.\n\nIf the tools aren't installed, the script prints the install commands. Don't try to install them silently — show the\nuser what needs to be added so they can decide whether to add them to CI.\n\n### 3. Run the policy check\n\nRun `scripts/check_policy.py .licenses-cache/`. This compares every detected license against the project policy in\n`references/license_policy.md`:\n\n- **Allowed** (green): MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Zlib, Unlicense, CC0-1.0, MPL-2.0 — proceed.\n- **Review** (yellow): LGPL-2.1, LGPL-3.0, EPL-2.0, CDDL-1.0, custom/unknown — pause and ask the user. LGPL is fine for\n  dynamic linking but Go statically links, so flag it for review.\n- **Forbidden** (red): GPL-2.0, GPL-3.0, AGPL-3.0, SSPL-1.0, BUSL-1.1, Commons Clause, \"non-commercial\" clauses — STOP.\n  Do not proceed with file generation. Report the offending package and ask the user how to handle it (replace, vendor,\n  or accept and document the consequence).\n\n**This step is non-negotiable.** A forbidden license slipping into THIRD-PARTY-LICENSES.md isn't just a documentation\nproblem — it's a contract breach with every customer. If `check_policy.py` returns a  non-zero exit code, surface the \nfailures prominently and stop.\n\n### 4. Generate the markdown\n\nRun `scripts/generate_md.py .licenses-cache/ > THIRD-PARTY-LICENSES.md`. This produces the file using the structure\ndescribed in `references/template.md`. Key points the generator handles:\n\n- Sorts entries alphabetically within each section.\n- Groups by component category (Go backend / npm frontend).\n- Deduplicates packages that appear at multiple versions (lists all versions on one line).\n- Renders a summary table at the top with license counts.\n- Adds a generation timestamp and a note pointing to `scripts/collect_licenses.sh` so the file is reproducible.\n\n### 5. Verify and commit\n\nAfter generation:\n\n1. Diff against the previous `THIRD-PARTY-LICENSES.md` (if it exists). New rows = new dependencies — confirm with the\n   user that they're expected.\n2. Verify the file isn't truncated and ends with the closing footer (`<!-- end of generated file -->`).\n3. Suggest the commit message: `chore(licenses): regenerate THIRD-PARTY-LICENSES.md for vX.Y.Z`.\n4. If a CI pipeline exists (`.github/workflows/`), suggest adding a job that runs `scripts/check_policy.py` on every\n   PR — that's how the project prevents regressions, not by re-checking manually each release.\n\n## Notes on edge cases\n\n**Vendored dependencies.** If the repo has a `vendor/` directory, `go-licenses` will read from it. That's fine —\nvendored code still ships and still needs attribution.\n\n**Dual-licensed packages.** Some packages are offered under \"MIT OR Apache-2.0\". Pick MIT (the simpler one) and note the\nchoice in the entry: `License: MIT (also available under Apache-2.0)`.\n\n**Packages with no detectable license.** `go-licenses` flags these as \"Unknown\". Don't ship the file with Unknowns —\nmanually inspect the repo, fill in the SPDX identifier, and if there's truly no license, treat the package as\nforbidden (no license = \"all rights reserved\", which is incompatible with redistribution).\n\n**Ansible / Terraform / OpenTofu / Pulumi / Bash.** Do NOT list them in `THIRD-PARTY-LICENSES.md`. They are invoked\nas separate OS processes (`os/exec`), not linked, not redistributed. Their licenses bind the user who installs them\non the host, not Semaphore UI's distribution. Listing them would imply otherwise and is an unnecessary signal to a\nreviewing customer's legal team. The \"Subprocess exception\" section in `references/license_policy.md` explains the\nreasoning if a reviewer asks.\n\n**First-party packages with no LICENSE.** The root module and any `replace … => ./<dir>` target (e.g. `./pro`) live\ninside this repo and don't need to be self-attributed. The collection script filters them out by the root module\nprefix from `go.mod`. If you see \"Unknown license\" rows for `github.com/<this-org>/<this-repo>/…`, the filter is\nworking correctly — those rows shouldn't appear in the final cache.\n\n**Embedded license texts.** For packages under MIT/BSD/ISC, the license requires preserving the copyright notice. The\ngenerator embeds the full LICENSE file text from each package's source. For Apache-2.0 packages, it embeds the NOTICE\nfile if one exists.\n\n## What this skill does NOT do\n\n- It does not generate a CycloneDX or SPDX SBOM. If the user wants a machine-readable SBOM (for compliance scanners,\n  customer attestations, etc.), point them at `syft packages dir:. -o cyclonedx-json` as a separate step.\n  THIRD-PARTY-LICENSES.md is the human-readable artifact.\n- It does not check for CVEs. That's a security concern, not a licensing one — use `govulncheck` and `npm audit` for\n  that.\n- It does not modify go.mod or package.json. The skill is read-only with respect to the dependency manifests; it only\n  generates the attribution document.\n\nSee `references/license_policy.md` for the full policy and `references/template.md` for the output format.\n",".claude/skills/semaphore-third-party-licenses/references/license_policy.md":"# License Policy for Semaphore UI\n\nThis is the source of truth for which open-source licenses are acceptable in\nSemaphore UI dependencies. The policy is enforced by\n`scripts/check_policy.py` — if you change anything here, mirror the change\nin that script.\n\nThe policy exists for two reasons:\n\n1. **Customer obligations.** Our MSA commits us to *not* shipping\n   components on terms that would require customers to license or assign\n   their IP. That rules out copyleft licenses with strong reciprocity\n   (GPL family, AGPL, SSPL).\n2. **Project sustainability.** Source-available-but-not-OSS licenses\n   (BUSL, Commons Clause, Elastic 2.0) carry use restrictions that conflict\n   with our open-source distribution model.\n\n## Allowed (no review needed)\n\nThese are permissive licenses with attribution-only obligations. Anything\nunder one of these can be added without further discussion.\n\n| SPDX ID | Name |\n|---------|------|\n| MIT, MIT-0 | MIT License |\n| Apache-2.0 | Apache License 2.0 |\n| BSD-2-Clause | BSD 2-Clause \"Simplified\" |\n| BSD-3-Clause | BSD 3-Clause \"New\" |\n| ISC | ISC License |\n| Zlib | zlib License |\n| Unlicense | The Unlicense |\n| CC0-1.0 | Creative Commons Zero |\n| MPL-2.0 | Mozilla Public License 2.0 (file-level copyleft, OK for libraries) |\n| 0BSD | BSD Zero Clause |\n| BlueOak-1.0.0 | Blue Oak Model License |\n| Python-2.0 | Python Software Foundation License |\n\n**Note on MPL-2.0:** MPL is allowed because its copyleft obligation is at\nthe *file* level, not the project level. We can use MPL-licensed libraries\nwithout making Semaphore UI MPL. If we ever modify an MPL-licensed file,\nthat file must remain MPL.\n\n## Review required\n\nThese licenses are not blanket-forbidden but require a case-by-case\ndecision. Pause and ask before merging a dependency under one of these.\n\n| SPDX ID | Why review |\n|---------|------------|\n| LGPL-2.1, LGPL-3.0 (and `-or-later`) | LGPL is fine for **dynamic** linking, but Go statically links by default. An LGPL Go library would force us to provide object files so users can relink. Acceptable only if the library is genuinely irreplaceable and we can document the relinking pathway. |\n| EPL-1.0, EPL-2.0 | Eclipse Public License has weak copyleft and an explicit patent clause. Generally workable but worth a legal eyeball. |\n| CDDL-1.0, CDDL-1.1 | File-level copyleft like MPL, but with extra requirements. Rare in our ecosystem. |\n| EUPL-1.1, EUPL-1.2 | European Union Public License. Compatible with several other licenses; check the specific version. |\n| OFL-1.1 | SIL Open Font License. Almost always fine for embedded fonts, but verify the font is being used as intended (not redistributed standalone with a new name). |\n\n## Forbidden\n\nA dependency under one of these licenses **must not be added**. If\n`check_policy.py` flags one, the options are: (a) remove it, (b) replace\nit with a permissively-licensed alternative, (c) move the functionality\nout of process (subprocess invocation breaks the linkage in most cases —\nthis is why Ansible is acceptable).\n\n| SPDX ID | Why forbidden |\n|---------|--------------|\n| GPL-2.0, GPL-3.0 (and `-only` / `-or-later`) | Strong copyleft. Linking forces Semaphore UI to be GPL, which conflicts with our distribution model. |\n| AGPL-1.0, AGPL-3.0 | Same as GPL plus a network-interaction trigger. Particularly hostile to SaaS/self-hosted server software. |\n| SSPL-1.0 | MongoDB's Server Side Public License. Not OSI-approved; imposes obligations on anyone offering the software as a service. |\n| BUSL-1.1 | Business Source License. Time-delayed open source with use restrictions during the change window. Not OSS. |\n| Commons-Clause | Restricts commercial sale; strips OSS status from any underlying license it's attached to. |\n| Elastic-2.0 | Elastic License v2. Use restrictions; not OSS. |\n| RSALv2 | Redis Source Available License v2. Same family as BUSL/Elastic. |\n| CC-BY-NC-* | Creative Commons \"Non-Commercial\" variants. Not usable in a commercially-distributable product. |\n| Facebook-BSD-Patents (legacy) | The old React patent grant clause. Removed years ago, but flagged here as a defensive measure. |\n\n## Subprocess exception (External Runtime Tools)\n\nTools that Semaphore UI invokes via `os/exec` (Ansible, Terraform, OpenTofu,\nPulumi, Bash) are NOT linked into our binary and NOT redistributed by us.\nTheir licenses govern the user's installation of those tools on the host\nsystem, not our distribution.\n\nThis is why we can integrate with Ansible (GPL-3.0) and Terraform (BUSL-1.1)\nwithout violating this policy — we never embed their code, we only document\nthat the user must install them separately.\n\nThese tools are intentionally **not listed** in `THIRD-PARTY-LICENSES.md`.\nListing them would wrongly imply that they are distributed with the product\nand would invite unnecessary scrutiny from a customer's legal team. Neither\n`collect_licenses.sh` nor `generate_md.py` produces or accepts an external-\ntools section.\n\n## When the policy itself needs to change\n\nIf a dependency we genuinely need is forbidden, the path forward is:\n\n1. Document the technical need (what does it do, what alternatives exist).\n2. Get sign-off from a maintainer with the authority to make that call.\n3. Update **both** this document and `check_policy.py`, in the same commit.\n4. Note the exception in `THIRD-PARTY-LICENSES.md` so customers can see it.\n\nDon't relax the policy silently. The whole point is that customers can\naudit us, so the policy must be auditable too.\n",".claude/skills/semaphore-third-party-licenses/references/template.md":"# THIRD-PARTY-LICENSES.md output format\n\nThis is the canonical structure that `scripts/generate_md.py` produces.\nReference for humans reviewing the generator output, and for anyone who\nneeds to understand what each section is for.\n\n## Structure\n\n```\n# Third-Party Licenses\n\n[intro paragraph explaining what this file is and why it exists]\n\n_Generated on YYYY-MM-DD HH:MM UTC by scripts/collect_licenses.sh._\n\n[regeneration instructions — always include these so the file is\n reproducible by anyone, not just the original author]\n\n## Summary\n\n[total component count]\n\n| Ecosystem | Components |\n|-----------|------------|\n| Go (backend)        | NN |\n| npm (frontend)      | NN |\n\n### License distribution\n\n| License | Count |\n|---------|-------|\n| MIT          | NN |\n| Apache-2.0   | NN |\n| BSD-3-Clause | NN |\n| ...          | NN |\n\n## Go Backend Dependencies\n\n[one-line description of what these are and where they come from]\n\n| Component | Version(s) | License | Source |\n|-----------|------------|---------|--------|\n| `github.com/foo/bar`  | v1.2.3 | MIT | [link](https://...) |\n| `github.com/foo/baz`  | v0.4.1, v0.5.0 | Apache-2.0 | [link](https://...) |\n| ...\n\n## Frontend Dependencies (npm)\n\n[one-line description]\n\n| Component | Version(s) | License | Source |\n|-----------|------------|---------|--------|\n| `vue`        | 3.4.21 | MIT | [link](https://...) |\n| `vuetify`    | 3.5.8  | MIT | [link](https://...) |\n| ...\n\n---\n\n## License texts\n\n[paragraph explaining where to find full license texts and how to report\n missing/incorrect attributions]\n\n<!-- end of generated file -->\n```\n\n## Why this structure\n\n**Summary table at the top.** Lets a reader (especially a customer's\ncompliance team) see the shape of the dependency tree at a glance\nwithout scrolling through hundreds of rows.\n\n**License distribution.** Surfaces concentration risk. If 95% of deps are\nMIT/Apache and 1 is MPL-2.0, that 1 deserves attention. It also makes it\neasy to spot a forbidden license that somehow slipped through (a single\nGPL-3.0 row in the distribution table is a very loud red flag).\n\n**Sorted alphabetically within each section.** Diffs between releases\nbecome useful — a new dependency shows up as an added row in one\npredictable place, not scattered across the file.\n\n**Versions collapsed onto one line.** A dependency pinned at multiple\nversions (common with transitive Go deps that get MVS-resolved differently\nin different parts of the tree) shouldn't take 5 rows. One row, comma-\nseparated versions.\n\n**External tools deliberately excluded.** Ansible, Terraform, OpenTofu,\nPulumi, and Bash are invoked as subprocesses, not linked, and not\nredistributed with Semaphore UI. They are not listed in this file —\nincluding them would wrongly imply distribution. The license policy\ndocument covers them under \"Subprocess exception\" if a reviewer asks\nwhy they're absent.\n\n**Regeneration instructions in the header.** Means the file is not a\nhand-maintained artifact that drifts. Anyone can rebuild it and verify\nit matches what's actually in the dependency manifests.\n\n**Footer comment marker.** `<!-- end of generated file -->` lets CI\nverify the file wasn't truncated mid-write — useful when generation runs\nin a pipeline and might fail partway.\n\n## What NOT to include\n\n- **Full license texts inline.** Don't copy the MIT license text 200\n  times into one file. Refer readers to the source URL. The packages\n  themselves carry their LICENSE files; the user has them on disk after\n  `go mod download` / `npm install`.\n- **CVE / vulnerability info.** That belongs in security advisories,\n  not the license document. Mixing the two confuses the reader and makes\n  the file go stale faster.\n- **Internal-only dependencies.** Build tooling (linters, test\n  frameworks, code generators) doesn't ship to customers. Production\n  dependencies only.\n- **Hand-edited rows.** If something needs a special note, add it via\n  the generator (extend `generate_md.py`), not by editing the output\n  file. Hand edits get destroyed on the next regeneration.\n",".github/copilot-instructions.md":"# Semaphore UI Development Instructions\n\n**Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**\n\nSemaphore UI is a modern web interface for managing popular DevOps tools like Ansible, Terraform, PowerShell, and Bash scripts. It's built with Go (backend) and Vue.js (frontend), using Task runner for build automation.\n\n## Working Effectively\n\n### Bootstrap, build, and test the repository:\n- Install Go 1.21+ (currently requires go version 1.21 or higher)\n- Install Node.js 16+ \n- Install Task runner: `go install github.com/go-task/task/v3/cmd/task@latest`\n- Install dependencies: `task deps` -- takes 3 minutes first time (faster with cache). NEVER CANCEL. Set timeout to 5+ minutes.\n- Build the application: `task build` -- takes 1.5 minutes. NEVER CANCEL. Set timeout to 3+ minutes.\n- Run tests: `task test` -- takes 33 seconds. NEVER CANCEL. Set timeout to 2+ minutes.\n\n### Run the application:\n- ALWAYS run the bootstrapping steps first\n- Setup database and admin user: `./bin/semaphore setup` (interactive, use SQLite option 4 for development)\n- Start server: `./bin/semaphore server --config ./config.json`\n- Web UI: http://localhost:3000 (login: admin / changeme)\n- API: http://localhost:3000/api/ (test with: `curl http://localhost:3000/api/ping`)\n\n## Validation\n\n- **CRITICAL**: Always manually validate any new code by building and running the application.\n- ALWAYS run through at least one complete end-to-end scenario after making changes:\n  1. Build the application: `task build`\n  2. Start the server: `./bin/semaphore server --config ./config.json`\n  3. Test API endpoint: `curl http://localhost:3000/api/ping` (should return \"pong\")\n  4. Access web UI at http://localhost:3000 and verify it loads\n  5. For auth changes: Test login with admin/changeme\n- For significant changes, run full setup process to ensure setup still works\n- Always build and exercise your changes before considering the task complete\n\n### Complete Validation Scenario (for major changes):\n```bash\n# 1. Clean build\ntask build\n\n# 2. Setup database (if config.json doesn't exist)\n./bin/semaphore setup  # Choose option 4 (SQLite), use admin/changeme\n\n# 3. Start server\n./bin/semaphore server --config ./config.json\n\n# 4. Test in another terminal\ncurl http://localhost:3000/api/ping  # Should return \"pong\"\ncurl -I http://localhost:3000/       # Should return HTTP 200\n\n# 5. Test web interface manually in browser at http://localhost:3000\n# 6. Test login with admin/changeme if auth-related changes\n```\n\n### Linting and Code Quality\n\n- Frontend linting: `cd web && npm run lint` (has known warnings about console statements and asset sizes - ignore existing issues)\n- Backend linting: `golangci-lint run --timeout=3m` (has known type errors due to module import issues - ignore existing issues) \n- **DO NOT** try to fix existing linting issues unless specifically asked to\n- Always run linting on new code you add to ensure it follows project standards\n- Install golangci-lint if needed: `go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2`\n\n## Common Tasks\n\n### Repository Structure\n```\n.\n├── README.md           - Project documentation  \n├── CONTRIBUTING.md     - Development guidelines\n├── Taskfile.yml       - Task runner configuration\n├── go.mod             - Go module dependencies\n├── web/               - Vue.js frontend application\n│   ├── package.json   - Frontend dependencies\n│   ├── src/           - Vue.js source code\n│   └── public/        - Static assets\n├── cli/               - Go CLI application entry point  \n├── api/               - Go API server endpoints\n├── db/                - Database models and interfaces\n├── services/          - Business logic services\n├── util/              - Utility functions and configuration\n├── bin/               - Built binaries (after build)\n└── config.json       - Runtime configuration (after setup)\n```\n\n### Key Commands Reference\n```bash\n# Install task runner\ngo install github.com/go-task/task/v3/cmd/task@latest\n\n# Install all dependencies (backend + frontend + tools)\ntask deps\n\n# Build application (frontend + backend)\ntask build\n\n# Run tests\ntask test  \n\n# Run linting\ntask lint\n\n# Setup application (interactive)\n./bin/semaphore setup\n\n# Start server\n./bin/semaphore server --config ./config.json\n\n# View available task commands\ntask --list\n```\n\n### Database Options for Development\n\nDuring setup, choose option 4 (SQLite) for the simplest development setup:\n\n- No external database server required\n- Database file stored at the path configured in `config.json` (default under `/tmp/`)\n- Perfect for development and testing\n\n> **Note:** BoltDB (option 2) was removed in Semaphore 2.19. If you have an existing\n> `database.boltdb` file, migrate to SQLite, MySQL, or PostgreSQL before upgrading.\n\n### Frontend Development\n- Vue.js 2.x application in `web/` directory\n- Built with Vue CLI and Vuetify components\n- Build output goes to `api/public/` for serving by Go backend\n- Development server not typically used - Go server serves built assets\n\n### Backend Development  \n- Go application with CLI and API server\n- Uses Gorilla Mux for routing\n- Supports multiple databases: MySQL, PostgreSQL, SQLite (BoltDB was removed in 2.19)\n- Configuration via JSON file or environment variables\n\n## Troubleshooting\n\n### Build Issues\n- If `task` command not found: Install with `go install github.com/go-task/task/v3/cmd/task@latest`\n- If Go version errors: Ensure Go 1.21+ is installed\n- If npm install fails: Ensure Node.js 16+ is installed\n- If build takes too long: This is normal - frontend build can take 60+ seconds\n\n### Runtime Issues  \n- If server won't start: Check config.json exists and database is accessible\n- If web UI shows errors: Check that frontend build completed successfully in `api/public/`\n- If API returns errors: Check server logs for specific error messages\n\n### Database Issues\n\n- For development, use SQLite (option 4) during setup\n- The SQLite database file is created automatically at the configured path\n- If database errors occur, remove the SQLite file and run setup again\n- Configs with `\"dialect\": \"bolt\"` will fail at startup — switch to `sqlite` and re-run setup\n\n## Important Notes\n\n- **NEVER CANCEL** long-running builds or dependency installations\n- Set appropriate timeouts: deps (5+ min), build (3+ min), tests (2+ min)  \n- The application serves the frontend from the Go backend - no separate frontend server needed\n- Configuration is stored in `config.json` after running setup\n- Default admin credentials after setup: admin / changeme\n- Linting has known issues - focus on not introducing new ones\n- Always test changes by running the full application, not just unit tests","AGENTS.md":"Read and follow .claude/CLAUDE.md before starting any work.\n","AGENTS/agents/setup/db-query/README.md":"# db-query\n\nAd-hoc SQL runners for the project's local databases — **one script per database**. Each runs the\nquery as that database's app user, read-only by default, and logs every query it runs.\n\n| Script | Engine | Mode | Database | `.env` file | `.env` keys | Access |\n| --- | --- | --- | --- | --- | --- | --- |\n| `query_<db>.sh` | PostgreSQL / MariaDB | docker / host | `<database>` | `<path>` | `DB_*` | read-only |\n\n## Setup\n\nNone. Each script reads its connection from the project's own `.env`; the path to it is\n`ENV_FILE` in the script's settings block. Credentials are never copied into this folder.\n\n## Usage\n\n~~~shell\nAGENTS/tools/db-query/query_<db>.sh \"SELECT id, email FROM users LIMIT 5\"\nAGENTS/tools/db-query/query_<db>.sh -f report.sql\necho \"SELECT 1\" | AGENTS/tools/db-query/query_<db>.sh\nAGENTS/tools/db-query/query_<db>.sh \"SELECT ...\" -t -A     # trailing flags go to psql / mariadb\n~~~\n\n## Log\n\nEvery query is appended to `AGENTS/tools/db-query/log/queries.log` before it runs — timestamp,\nscript, target, database, user, access mode, the SQL — and its exit code after. Result rows are not\nlogged. The folder is gitignored.\n\n## Traps\n\n- ⚠️ Local only: a remote docker daemon or a non-loopback host is refused. `ALLOW_REMOTE=1` in a\n  script is set only on the user's explicit request — record who asked and when here.\n- ⚠️ Read-only by default: the session refuses any write (`READ_ONLY=1`). It guards against\n  accidents, not against intent — SQL can switch it off. `READ_ONLY=0` in a script is set only on\n  the user's explicit request — record who asked and when here.\n","AGENTS/docs/INDEX.md":"# docs — portable standard by zone\n\nApproaches and conventions that would serve another project, not this project's own facts. One\nfolder per zone, each with its own `INDEX.md`. This file routes to a zone; the zone's index routes\nto the doc.\n\n## Zone index (example)\n\nThe rows below are example data, illustrating the shape a real routing table takes; replace them\nonce an actual zone exists.\n\n| When you need it | Read |\n| --- | --- |\n| Example: Writing backend code: models, routes, permissions, entity codes | `backend/INDEX.md` |\n| Example: Running or writing backend tests — mandatory before the first command | `backend/INDEX.md` |\n| Example: Editing the frontend: app layout, import boundaries, i18n | `frontend/INDEX.md` |\n| Example: Setting up a local stack: reverse proxy, HMR behind a proxy, tunnels — the approach, not this project's ports | `infra/INDEX.md` |\n| Example: Editing an env file or another human-read file: grouping, order, language, line length | `standards/INDEX.md` |\n","AGENTS/tools/wf-stand/README.md":"# wf-stand — run and verify workflows on the local stand\n\nDrives the agent's throwaway Semaphore stand (`http://localhost:3100`, admin/admin123, project 1,\nPro build from HEAD) to execute a workflow end to end and follow every node to a terminal status.\nExists because a workflow run is asynchronous: tasks, approvals and delays resolve over time and\nonly the run-details endpoint shows all of them together.\n\nCall forms (from the repository root):\n\n```\nAGENTS/tools/wf-stand/stand.sh {start|stop|restart|build|status|log [n]}\nAGENTS/tools/wf-stand/seed.sh\nAGENTS/tools/wf-stand/seed-outputs.sh          # PROJECT_NAME=\"…\" to seed another project\nAGENTS/tools/wf-stand/run.sh <workflow_id> [--approve|--reject] [--stop-after SEC] [--expect success|failed|stopped] [--timeout SEC] [--run ID]\nAGENTS/tools/wf-stand/runner.sh {start|stop|status|tag|untag}   # PID=<project> TAG=<tag>; remote runner for the stand\nAGENTS/tools/wf-stand/inputs.sh <workflow_id> [run_id]           # outputs stored per producer + \"INPUT name=value\" lines per consumer\nNODE_PATH=~/.npm/_npx/9833c18b2d85bc59/node_modules node AGENTS/tools/wf-stand/shoot-run.cjs <workflow_id> [run_id] [out.png] [--dark]\nAGENTS/tools/wf-stand/seed-docs.sh                                # project \"Demo Stand\" + workflow \"Build and deploy\" for the docs screenshots\nAGENTS/tools/wf-stand/seed-survey.sh                              # workflow \"Survey: workflow variables\" (survey + node bindings) in the seed-outputs project\nNODE_PATH=… node AGENTS/tools/wf-stand/shoot-editor.cjs <workflow_id> [node_title] [out.png] [--dark]   # editor with a node selected: survey panel, canvas, bindings\nNODE_PATH=… node AGENTS/tools/wf-stand/shoot-run-dialog.cjs <workflow_id> [out_dir] [--dark]       # starts a run through the survey dialog; shoots the dialog and the run view's Variables strip\nPID=<project> WF=<workflow> NODE_PATH=… node AGENTS/tools/wf-stand/shoot-docs.cjs   # the four screenshots of workflows.md § Outputs and inputs\n```\n\nExamples:\n\n```\n$ AGENTS/tools/wf-stand/stand.sh build && AGENTS/tools/wf-stand/stand.sh start\nstand: up at http://localhost:3100 (pid 40366)\n$ AGENTS/tools/wf-stand/seed.sh\nworkflows happy=5 branches=6                      # ids also in /tmp/semaphore-stand/wf-exec-ids.txt\n$ AGENTS/tools/wf-stand/run.sh 5 --approve --expect success\nrun 4 started (workflow 5) — UI: http://localhost:3100/project/1/workflows/5/runs/4\n17:30:14 approval version=0.1.0 n18/task=task#6:success n19/approval=approval:pending n20/delay=- n21/task=-\n17:30:14 node 19 -> approved\n17:30:24 success version=0.1.0 n18/task=task#6:success n19/approval=approval:approved n20/delay=delay:success n21/task=task#7:success\nfinal: success (run 4)\n$ AGENTS/tools/wf-stand/run.sh 6 --stop-after 8 --expect stopped\nfinal: stopped (run 6)\n```\n\nSeeded workflows: \"Exec happy\" (task → approval → delay 5 s → task, ends `success` with `--approve`)\nand \"Exec branches\" (ok → fail; on_failure → recover, on_success → never started; always → 30 s\nslow task; ends `failed`). Templates are `bash` scripts from the local repo\n`/tmp/semaphore-stand/wf-repo` (`ok.sh`, `fail.sh`, `slow.sh`), so no network or Ansible is needed.\n\n`seed-outputs.sh` seeds the project \"Artifacts QA\" (project 1 since the 2026-10-07 rebuild) for\ntesting workflow outputs by hand: 16 templates (bash producers for every rule of the contract —\nvalid, empty, bad JSON, bad name, 101 outputs, 40 KB value, outputs-then-exit-1, symlink; bash and\nAnsible consumers with one survey var of every type; Terraform / Tofu with a `sensitive` and a\nhyphenated output; Ansible `set_stats`) and four workflows: 1 \"producers -> consumers\" (expect\n`success`, see `task.artifacts` of tasks of nodes 2–4), 2 \"invalid producers\" (expect `failed`,\neach bad producer `error` with the reason in its log, \"empty\" `success` without artifacts),\n3 \"failed producer\" (expect `failed`, no artifacts stored, `on_failure` consumer ran), 4 \"tofu ->\nconsume\" (expect `success`). Scripts: `/tmp/semaphore-stand/wf-repo/produce*.sh`, `consume.sh`,\n`tf/main.tf`, `playbooks/*.yml`. Ids land in `/tmp/semaphore-stand/wf-outputs-ids.txt`.\n\nSince 2026-10-08 the same seed adds the mapping checks: templates \"bash: produce renamed outputs\" and\n\"ansible: set_stats renamed outputs\" (output names `tag`, `count`, `flag`, `subnets`, `cfg`, `envname` that\nmatch no survey variable, so only an explicit edge mapping can deliver them) and workflows 9 \"Inputs: explicit\nmapping\" (renamed producers → bash and ansible consumers over `explicit` edges, `image_tag←tag`,\n`replicas←count`, …), 10 \"Inputs: by_name through approval\" (produce → approval → consumers, run with\n`--approve`), 11 \"Inputs: fan-in by_name vs explicit\" (produce by name and renamed explicitly into one consumer\nwith convergence all: expect `image_tag` from the explicit edge, `replicas` by name), 12 \"Inputs: explicit empty\nlist\" (the edge passes nothing). Consumers print one `INPUT name=value` line per survey variable (bash: parsed\n`k=v` args plus `env:env_name` — a `target: env` variable keeps its own name, it is not upper-cased; ansible: a debug list), so `inputs.sh <wf>` shows producer outputs next to what\nthe consumer received. Expected since the resolver landed (2026-10-08): workflow 1 and 10 — the consumer's\n`INPUT` lines repeat the producer's `image_tag`, `replicas`, `enabled`, `subnet_ids`, `config` (by name;\n`env-name` with a hyphen is not delivered); workflow 9 — the renamed values (`image_tag=renamed-…`,\n`replicas=7`, `env:env_name=prod`); workflow 11 — `image_tag=renamed-…` from the explicit edge and `replicas=3`\nby name; workflow 12 — only `image_tag=static-from-node`, `replicas=1` (the default) and `env:env_name=static-env`;\nworkflow 3 — the on_failure consumer is `error` before start with \"Required input(s) without a value: image_tag\"\nin its log. The task log of every consumer starts with the engine's `Input \"<var>\" <- output \"<key>\" of node N\n(task #M)` lines, so a value's source is always one line above the value.\n\nSince 2026-10-09 the seed also covers the Jinja rule (Ansible never evaluates an expression inside an\nextra var — every value reaches it through a file, the ones with a Jinja2 delimiter tagged `!unsafe`): template \"bash: produce jinja outputs\"\n(`produce_jinja.sh` writes `image_tag`, `config` and `replicas` holding `{{ lookup('pipe', …) }}`,\n`{{ lookup('env', 'HOME') }}`, `{{ 6*7 }}`), the variable group \"vars group with jinja\" (`group_expr: \"{{ 2*21 }}\"`),\ntemplate \"ansible: consume + vars group\" and workflows 13 \"Inputs: jinja injection\" (→ ansible and bash\nconsumers) and 14 \"Inputs: jinja vs vars group\". Expected on the server and on the runner: `INPUT image_tag={{\nlookup('pipe', 'echo INJECTED-$(id -un)') }}`, `config` with the expressions verbatim, `replicas=1` (the\ndefault — `{{ 6*7 }}` is not an int) and `INPUT group_expr={{ 2*21 }}` — the variable group is data too. A\nconsumer log containing `INJECTED-` or `group_expr=42` means Ansible evaluated a value: a regression.\n`inputs.sh` right after `run.sh` on the runner can miss the last log lines; re-run it with the run id.\nTrap: the template API ignores `environment_id`, pass `environment_ids`.\n\nSince 2026-10-10 the stand also checks the secret extra vars (TASK@CEA87DDD08): the survey secret\n`token` and the variable-group secret `db_password` (type `var`, added to \"vars group with jinja\" by hand\nthrough `PUT /project/1/environment/1` with `secrets:[{type:\"var\",name,secret,operation:\"create\"}]`) reach\nAnsible through a second, vault-encrypted `--extra-vars @task_<id>_secret_vars_*.yml` with a one-off\n`--vault-id <guid>@prompt`. `playbooks/consume.yml` prints `INPUT token=…` and `INPUT db_password=…`;\ntemplate 37 \"ansible: consume + own vault\" (access key \"tpl vault\", type login_password, vault `default`)\nruns `playbooks/consume_vault.yml`, which loads `playbooks/vaulted.yml` (encrypted by `ansible-vault` with\nthat key's password) through `vars_files` and prints `INPUT vaulted_var=…`. Expected on the server and on\nthe runner: `INPUT token=s3cr3t {{ 1+1 }}`, `INPUT db_password=p@ss {{ 7*6 }}` literally; `ps -axo args=`\nshows `--vault-id=<guid>@prompt` and two `--extra-vars @`, never a secret or the password; `grep -rl\ns3cr3t /tmp/semaphore-stand/tmp` (or `runner-tmp`) finds nothing while the task runs; no `task_*_vars_*`\nfile is left after. With `SEMAPHORE_LOG_LEVEL=DEBUG SEMAPHORE_DEBUG_FILTER=extra_vars` exported before\n`stand.sh start` / `runner.sh start`, the server and runner logs show seven `context=extra_vars` lines per\ntask (split, two files written, vault added, two files removed, password forgotten). Checked 2026-10-10:\nserver task 162, runner task 164, own vault task 171, regression runs 39 (wf 13) and 40 (wf 14).\nTraps: a vault-encrypted file must not sit in `playbooks/group_vars/` — Ansible loads it for every playbook\nof the directory and a template without that vault fails with \"Decryption failed\"; `vars_files` of the\nrepository are still templated (`{{ 3*3 }}` prints `9`) — only values Semaphore passes are `!unsafe`;\n`runner.sh start | tail` hangs because the runner inherits the pipe — redirect to a file instead; a PUT of a\ntemplate without the `vaults` field deletes its vaults, which is why `runner.sh tag/untag` re-reads each\ntemplate by id before the PUT.\n\nDocs screenshots (2026-10-11): `seed-docs.sh` seeds a separate project \"Demo Stand\" (the name the published\neditor screenshot already shows) with \"Provision network\" (bash, outputs `vpc_id`, `subnet_ids`), \"Build image\"\n(bash, outputs `image_tag`, `replicas` — the values quoted in the user guide), \"Deploy to cluster\" (ansible\n`playbooks/deploy.yml`, survey `image_tag`, `replicas`, `subnets`) and the workflow \"Build and deploy\":\nProvision → Build → Deploy by name plus Provision → Deploy explicit (`subnets ← subnet_ids`). Run it once\n(`PID=<p> run.sh <wf> --expect success`), then `shoot-docs.cjs` writes `workflow-inputs-explicit.png`,\n`workflow-inputs-by-name.png`, `workflow-run-outputs.png` and `task-outputs.png` (dialog clipped) to\n`/tmp/semaphore-stand/shots-docs`; convert with `cwebp -q 88` into `docs/static/assets/`. Traps: a workflow must\nhave exactly one root node (the explicit edge alone would make Provision and Build two roots); key hints need a\nrun of the current revision, so re-run after any re-seed; the editor fits a three-node graph at ~45 % on a 1440 px\nviewport — the script collapses the palette (`.WorkflowEditor__sideToggle`) and clicks the fit control after the\nside panel opens instead of zooming in, which pushes nodes under the panel.\n\nSeeding traps learned there: a Terraform-family template must be POSTed without `inventory_id`\n(the API creates a `terraform-workspace` inventory; a shared one gets claimed and hidden) and with\n`task_params.auto_approve`, or a plan with changes parks the task in `waiting_confirmation`; a\nworkflow must have exactly one root node; `survey_vars[].default_value` is a plain string; a JSON\n`\\\"` inside `$( )` inside a double-quoted bash string is mangled — build such fragments in a\nvariable first.\n\nTraps: `run.sh` exits 1 when `--expect` does not match or `--timeout` (default 180 s) passes;\n`--approve` resolves every pending approval it sees, so use `--run ID` to only follow an existing\nrun. `stand.sh build` rebuilds the Vue app into `api/public` (a tracked-ignored embed input) and\nthe binary at `/tmp/semaphore-stand-bin`; it needs the `pro_impl` workspace for Workflows to be\nenabled. The stand DB is `/tmp/semaphore-stand/database-wf.sqlite` and persists between sessions.\n\n`runner.sh` registers a global runner \"wf-runner\" (tag `wf`, local executor, `/tmp/semaphore-stand/runner-config.json`,\nlog `runner.log`) and starts it from the same binary; `tag` sets `runner_tag` on every template of project\n`PID` (default 1) so their tasks go to it, `untag` sends them back to the server. The remote-runner check\nof workflow outputs (2026-10-08): `runner.sh start && runner.sh tag && run.sh 1 --expect success && run.sh 2\n--expect failed`, then `sqlite3 /tmp/semaphore-stand/database-wf.sqlite \"select id,status,runner_id,length(artifacts)\nfrom task order by id desc limit 13\"` shows `runner_id` of the runner and `artifacts` on the producers only.\nTraps: the token is returned once at creation, so a runner whose config file is gone is deleted and recreated;\nstop the runner and `untag` before a session that expects tasks to run on the server, or they wait forever;\n`status` keeps saying `online` for up to 120 s after `stop` (the server's heartbeat timeout), trust the\n`process:` line.\n\n`seed-survey.sh` (2026-10-16, TASK@B197C72B7F) needs the templates of `seed-outputs.sh` and adds workflow\n\"Survey: workflow variables\": survey `env` (enum, default `staging`), `image_tag` (required), `replicas` (int,\ndefault 2); node 2 \"bash: consume\" bound `image_tag←image_tag`, `replicas←replicas`, `env_name←env` on top of\nstatic `image_tag=static-from-node`; node 3 the same template without bindings (must receive nothing although\nthe names match); node 4 \"bash: produce renamed outputs\" → node 5 \"bash: consume\" over an explicit edge\n`replicas←count` plus the binding `image_tag←image_tag`. Start a run with a body:\n`POST …/workflows/<id>/runs {\"environment\":{\"image_tag\":\"1.2.3\",\"env\":\"prod\"}}` (ids in\n`/tmp/semaphore-stand/wf-survey-ids.txt`). `shoot-editor.cjs` logs in, opens the editor, clicks the first card\nwhose title matches, scrolls the first bound survey field into view and writes the page, a crop of the task\nparams card (`<out>-fields.png`) and the open assign menu (`<out>-assign.png`). `shoot-run-dialog.cjs` opens the\nworkflow page, presses Run, checks that an empty required answer is refused, fills `image_tag` (`IMAGE_TAG`,\ndefault 1.2.3) and the first enum option, starts the run and shoots `run-dialog-invalid.png`, `run-dialog.png` and\n`run-view-variables.png` in `out_dir`; it prints the run id and the Variables chips.\n\n⚠️ 2026-10-16: macOS purges files (not directories) under `/tmp` that were not touched for ~3 days —\n`config-wf.json`, `database-wf.sqlite`, the admin user and every script in `wf-repo` vanished between sessions.\n`stand.sh build` + a fresh `config-wf.json` (sqlite at `$DIR/database-wf.sqlite`, port `:3100`, three\n`openssl rand -base64 32` keys, `use_remote_runner: true`), `migrate`, `user add --admin --login admin\n--password admin123` and the seeds bring it back; `wf-repo` scripts must be recreated before any run.\n","AGENTS/work/workflow-editor/mocks/README.md":"# mocks — approved mock, screenshots and the scripts that shoot them\n\nDesign artefacts of the editor redesign (`TASK@88179c9bc1`); decisions are in\n`../editor-architecture.md`, not here. Every script targets the local stand\n`http://localhost:3100` (admin/admin123, project 1) and Playwright loaded through `NODE_PATH`\nwith `channel: 'chrome'` — stand and Playwright recipes are in `AGENTS/memory/ui-stands.md`.\n\n## Files\n\n| File | What it is |\n| --- | --- |\n| `2026-09-26-workflow-node-mock.html` | Approved static mock: editor canvas, run view, card states, empty state; `#dark` in the URL flips the theme. Open from the repo checkout — MDI icons load from `web/node_modules`. Its \"Tab opens quick-add\" hint is stale (see ⚠️ in `editor-architecture.md`). |\n| `2026-09-26-workflow-node-mock-{light,dark}.png` | Renders of the mock as approved. |\n| `workflow-editor/editor-{light,dark}.png`, `editor-selected-*.png`, `editor-quickadd-light.png`, `editor-legacy-*.png`, `editor-empty-light.png`, `run-{light,dark}.png` | Screenshots of the shipped UI at 1440 × 900 @2x, shot by `shoot-wf.cjs`. \"Legacy\" is a workflow saved without positions (auto layout). |\n| `workflow-editor/editor-interactions.png` | End state of the `interact-wf.cjs` checklist. |\n| `workflow-editor/run-approval-pending.png`, `run-delay-countdown.png` | Run view with a pending approval and the delay countdown, from `approve-wf.cjs`. |\n| `workflow-editor/editor-edge-explicit-{light,dark}.png`, `editor-edge-byname-{light,dark}.png`, `run-outputs-{light,dark}.png`, `task-outputs-{light,dark}.png` | Workflow inputs (2026-10-09, `TASK@1845d9d4d7` stage 7): the edge panel with \"Map inputs explicitly\" and the mapping table (keys suggested from the last run), the by_name panel with the destination's survey variables (a check on those the source produced), the run view with \"N outputs\" on producer cards and the swap icon on explicit pills, the task dialog's Outputs panel. Shot by `shoot-inputs.cjs` on the \"Artifacts QA\" project. |\n| `survey/editor-bindings.png`, `survey/editor-bindings-fields.png`, `survey/editor-bindings-assign.png`, `survey/editor-problem.png` | Workflow survey variables (2026-10-16, `TASK@B197C72B7F` stage 7, redesigned the same day on the owner's request): the editor with the survey in the left panel, \"3 from workflow\" on a bound card and the node panel whose bound survey fields are read-only with a link icon and an unbind cross; a crop of the task params card; the \"assign a workflow variable\" menu open next to an unbound field; the same workflow after the variable \"Image tag\" was deleted — \"2 problems\", Save disabled, badges on the bound nodes and the error under the orphaned field. Shot by `AGENTS/tools/wf-stand/shoot-editor.cjs` on workflow 11 of \"Artifacts QA\" (`seed-survey.sh`). |\n| `survey/run-dialog.png`, `survey/run-dialog-invalid.png`, `survey/run-view-variables.png` | Workflow survey at run time (2026-10-16, `TASK@B197C72B7F` stage 8): the \"Run\" dialog with the survey fields seeded from the defaults, the same dialog refusing an empty required answer, and the run view with the \"Variables\" strip (`run.environment`) under the toolbar. Shot by `AGENTS/tools/wf-stand/shoot-run-dialog.cjs` on workflow 11 of \"Artifacts QA\". |\n| `workflow-editor/editor-revision-active-run.png`, `run-after-edit.png` | Revisions (2026-09-27): the editor with the \"rev. 3\" and \"1 active run\" chips, and run #8 still rendered on its revision 1 after two later saves. Shot with `AGENTS/tools/wf-stand/shoot-run.cjs`. |\n\n## Scripts\n\n| Script | Does | Needs |\n| --- | --- | --- |\n| `seed-wf.sh` | Seeds five templates (one with an XSS name), the \"Release\" workflow with positions, the position-less \"Nightly checks\" (with a `<script>` note), starts one Release run; writes ids to `/tmp/semaphore-stand/wf-ids.txt`. Idempotent for templates. | Running stand, repository 1 and inventory 1 in project 1. |\n| `shoot-wf.cjs` | Editor/run/legacy/empty screenshots in light and dark; output `OUT` (default `/tmp/semaphore-stand/shots-wf`), `RUN` selects the run id. | `wf-ids.txt`. |\n| `interact-wf.cjs` | 16 PASS/FAIL checks on the editor: quick-add from the port, palette click, drag connect, cycle guard, pill condition change, snap, Delete, undo/redo round trip, problem badge + disabled Save, leave guard. Exits 1 on any FAIL or page error. | `wf-ids.txt`. Mutates the Release workflow in the browser but does not save. |\n| `approve-wf.cjs` | Approve inside the card, delay countdown ticks, viewport survives polling, task log opens on click. | `/tmp/semaphore-stand/wf3.txt` with `<workflow id> <run id>` of a run waiting at an approval — seeded by hand, not by `seed-wf.sh`. |\n| `keys-wf.cjs` | Keyboard pan/zoom/fit/reset in the editor and the run view; asserts no effect while typing in the properties panel. | `wf-ids.txt`, run 2 of Release. |\n| `shoot-inputs.cjs` | Shoots the four inputs screenshots above and runs 7 PASS/FAIL checks (checkbox state, mapping rows, hint line, by_name chips with a matched output, output count on a card, Outputs panel in the task dialog). `--dark` for the dark theme; `WF` (default 9, explicit workflow), `WF_BYNAME` (default 1), `OUT` (default `/tmp/semaphore-stand/shots-inputs`). Edges are selected by a dispatched `mousedown` on the path — a real click lands on the condition pill. | `AGENTS/tools/wf-stand/seed-outputs.sh` seeded, a finished run of the current revision of both workflows (`run.sh 9`, `run.sh 1`) for the key hints. |\n| `interact-inputs.cjs` | 10 PASS/FAIL checks of the inputs round trip on workflow `WF` (default 12): select the explicit edge, not dirty before editing, type a key, dirty, explicit icon on the pill, Save → API shows the pair, uncheck → by_name chips, Save → fields gone, original graph restored by PUT. Exits 1 on any FAIL or page error. | Running stand with the seeded project; mutates workflow 12 and restores it. |\n| `shoot-nodes.cjs` | Crops one card per node kind (task hovered with the \"+\", approval, delay, note) with edge pills hidden and zoom 1; source of `docs/static/assets/workflow-node-*.webp`. | `WF` env = id of the \"Docs node kinds\" workflow on the stand (default 4). |\n| `make-hotkeys-svg.py <out.svg>` | Generates the keyboard cheat-sheet `docs/static/assets/workflow-hotkeys.svg`; edit `COLUMNS` and re-run when a shortcut changes. | Python 3 only. |\n","CLAUDE.md":"AGENTS/agent-primary.md"},"files":{".claude/CLAUDE.md":"# Claude Code Instructions\n\n# Documentation\n\nThe documentation stored in /docs directory. It is a submodule with official Semaphore UI documentation\nin on Docusaurus engine.\n\nYou should actualize the documentation for new and updated featues.\n\n### Generated reference pages\n\n`docs/docs/reference/configuration.md` and `docs/docs/reference/cli/commands.md` are generated\nfrom this repository and must never be edited by hand:\n\n- `tools/docsref` reads the struct tags and doc comments of `util.ConfigType`.\n- `tools/clidocs` walks the real Cobra command tree from `cli/cmd`.\n\nAfter changing a configuration option or a CLI command, run `task docs:gen` and commit\nthe result together with the code. The `docs-reference` job in `.github/workflows/dev.yml`\nfails when the committed pages differ from what the generators produce.\n\nA config field with no doc comment falls back to `tools/docsref/descriptions.json`.\nPrefer writing the doc comment: `docsref` prints how many options still depend on that\nfile, and the number should only fall.\n\n## Writing Plans\n\nAll plans, tasks, researches for AI agents stored in folder AGENTS.\n\nEach plan has markdown-format and stored in folder AGENTS/plans/<version>.\n\nPlan can be split to tasks. Each task describes in details how to implement some part of some plan.\n\n## Code Style\n\n1. Do not use global variables. Global variables are forbidden.\n\n## High Availability Support\n\nAll solutions must work in High Availability mode.\n\n## Security Is the #1 Priority\n\nAll solutions must be secure by design. Do not consider any solution that introduces security risks.\n\n## How to do research\n\nUse MCP server `research` if your asks you to research something.\n\n## Writing Tests\n\nUse `github.com/stretchr/testify/assert` (and `require` when a failure should stop the test immediately).\n\n### Run tests\n\n```bash\ngo test ./path/to/package/ -run TestFunctionName -v -count=1\n```\n\n### Rules\n\n- Test file goes next to the source: `foo.go` → `foo_test.go`, same package.\n- Use `assert.Equal`, `assert.Empty`, `assert.Nil`, `assert.True`, `assert.Panics`, etc. Never use raw `if`/`t.Fatalf` for assertions.\n- Use `require` (instead of `assert`) when subsequent lines depend on the check passing (e.g., `require.NoError` before using a result).\n- Use table-driven tests with `t.Run` for multiple inputs to the same function.\n- Use `t.TempDir()` for temporary files — cleanup is automatic.\n- When testing code that uses package-level globals (e.g., `util.Config`), initialize them in a helper and reset state between tests.\n- When testing code that reads `os.Stdin`, swap it with `os.Pipe()` and restore in `defer`.\n\n### Template\n\n```go\npackage mypkg\n\nimport (\n    \"testing\"\n\n    \"github.com/stretchr/testify/assert\"\n    \"github.com/stretchr/testify/require\"\n)\n\nfunc TestMyFunction(t *testing.T) {\n    // setup\n    input := \"value\"\n\n    // act\n    result, err := MyFunction(input)\n\n    // assert\n    require.NoError(t, err)\n    assert.Equal(t, \"expected\", result)\n}\n\nfunc TestMyFunction_TableDriven(t *testing.T) {\n    tests := []struct {\n        name     string\n        input    string\n        expected string\n    }{\n        {\"short input\", \"abc\", \"ABC\"},\n        {\"empty input\", \"\", \"\"},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            assert.Equal(t, tt.expected, MyFunction(tt.input))\n        })\n    }\n}\n\nfunc TestMyFunction_Panics(t *testing.T) {\n    assert.Panics(t, func() {\n        MyFunction(nil)\n    })\n}\n```\n\n### Common assertions\n\n| Assertion | Use when |\n|-----------|----------|\n| `assert.Equal(t, expected, actual)` | Comparing values |\n| `assert.NotEqual(t, a, b)` | Values must differ |\n| `assert.Nil(t, val)` / `assert.NotNil(t, val)` | Pointer/interface checks |\n| `assert.Empty(t, val)` | String, slice, or map is zero-length |\n| `assert.Contains(t, str, substr)` | Substring or element in collection |\n| `assert.True(t, cond)` / `assert.False(t, cond)` | Boolean conditions |\n| `assert.NoError(t, err)` / `assert.Error(t, err)` | Error checks |\n| `assert.ErrorContains(t, err, \"msg\")` | Error with specific message |\n| `assert.Panics(t, func(){...})` | Code must panic |\n| `require.NoError(t, err)` | Stop test immediately on error |\n\n### Initializing `util.Config` in tests\n\nMany packages depend on `util.Config` which is a `*ConfigType` pointer (nil by default). Initialize before use:\n\n```go\nfunc setupConfig() {\n    if util.Config == nil {\n        util.Config = &util.ConfigType{}\n    }\n    // Initialize nested pointers as needed:\n    if util.Config.Runner == nil {\n        util.Config.Runner = &util.RunnerConfig{}\n    }\n}\n```\n\n### HTTP handler tests\n\nUse `net/http/httptest`:\n\n```go\nfunc TestMyHandler(t *testing.T) {\n    req := httptest.NewRequest(http.MethodGet, \"/api/endpoint\", nil)\n    w := httptest.NewRecorder()\n\n    myHandler(w, req)\n\n    assert.Equal(t, http.StatusOK, w.Code)\n    assert.Contains(t, w.Body.String(), \"expected\")\n}\n```\n",".claude/skills/semaphore-checkout/SKILL.md":"---\nname: semaphore-checkout\ndescription: >-\n    Create and switch to a branch with the same name in both repos — the root repo and pro_impl. \n    Usage: /semaphore-checkout <branch_name>\n---\n\nCreate a branch named `<branch_name>` in both repositories at once — the root repo\n(current directory) and the PRO module repo (`./pro_impl`) — and switch both to it.\n\nCI depends on branch names matching: GitHub Actions clones the `pro_impl` branch\nwith the same name as the root repo branch and falls back to `main` if it does not\nexist. This skill keeps the two repos in sync.\n\n## Arguments\n\n- `<branch_name>` (required) — the branch to create. If not provided, ask the user\n  for it. Validate it with `git check-ref-format --branch <branch_name>` before use.\n\n## Behavior\n\nFor each repo — root (`.`) first, then `./pro_impl`:\n\n1. Check the working tree is clean (`git status --porcelain`). If there are\n   uncommitted changes in either repo, stop and report which repo is dirty —\n   do not stash or discard anything.\n2. Fetch the latest state: `git fetch origin`.\n3. Create the branch from the up-to-date default branch and switch to it:\n   - root repo: `git checkout -b <branch_name> origin/develop`\n   - pro_impl: `git checkout -b <branch_name> origin/main`\n4. If the branch already exists locally, just `git checkout <branch_name>`\n   instead of creating it. If it exists only on origin, check it out tracking\n   the remote branch.\n\nDo not push the new branches — they are pushed later with the first commit.\n\n## Rules\n\n- Never force-create (`-B`) or reset an existing branch.\n- Both repos must end up on the same branch name; if step for `pro_impl` fails\n  after the root repo already switched, report the inconsistent state clearly.\n- Report the final state at the end: branch name and start point in each repo.\n\n## Example\n\n```\n/semaphore-checkout fix/task-env-vars\n```\n\nResult:\n\n```\nRoot repo:  fix/task-env-vars (from origin/develop)\npro_impl:   fix/task-env-vars (from origin/main)\n```\n",".claude/skills/semaphore-cli-docs/SKILL.md":"---\nname: semaphore-cli-docs\ndescription: >\n  Generate or update the CLI documentation under `docs/docs/reference/cli/` from the Cobra command code in\n  `cli/`. Use this skill whenever the user asks to \"update the CLI docs\", \"actualize/sync the CLI documentation\",\n  \"document the CLI\", \"the CLI docs are out of date\", \"regenerate the runner/user/vault/project/migrate docs\", or\n  after adding or changing a Cobra command, subcommand, flag, alias, or required-argument check anywhere under\n  `cli/cmd/`. Covers every command group (`users`, `projects`, `vaults`, `runner`, `migrate`) plus the operational\n  commands (`server`, `setup`, `version`) and the global flags on the root command. Trigger when the user adds a\n  new `*Cmd` Cobra command, a new `PersistentFlags()` call, or renames/removes a CLI flag.\n---\n\n# Semaphore — CLI Documentation Generator\n\nThis skill produces and maintains the CLI reference pages under\n`docs/docs/reference/cli/` and the CLI index `docs/docs/reference/cli/README.md`. Every page is **derived from the\nCobra command code** in `cli/cmd/`. If a command, subcommand, or flag isn't in the code, it must not be in the\ndocs; if a flag's name, default, or required-ness changed, the docs must change with it.\n\n> `docs/` is a **git submodule** (the documentation site). Editing files under `docs/docs/...` and\n> `docs/sidebars.js` is correct; those changes are committed to the submodule, not the main repo.\n\nThe docs are the human-facing contract. The Go code is the source of truth. When they disagree, the code wins —\n**but** read the command's `Run` body, not just its help strings, because help text drifts (see Pitfalls).\n\n> **`docs/docs/reference/cli/commands.md` is not yours.** It is generated by `tools/clidocs`, which walks the real\n> Cobra tree and emits every command and flag with its Cobra help string. Run `task docs:gen` for it and never edit\n> it by hand. This skill owns the *task-oriented* pages next to it: what each command group is for, what the\n> arguments mean in practice, what the command actually does to the database, and worked examples. Do not try to\n> reproduce an exhaustive flag inventory here — link to `/reference/cli/commands` instead and spend the words on\n> behaviour the help strings cannot express.\n\n## Inputs (source of truth)\n\nRead these in full before writing. Each command group maps to exactly one page.\n\n| Source files (`cli/cmd/`) | Doc page |\n|---------------------------|----------|\n| `root.go` | `docs/docs/reference/cli/README.md` — global flags, command overview |\n| `server.go`, `setup.go`, `version.go` | `docs/docs/reference/cli/README.md` — operational commands |\n| `user.go`, `user_add.go`, `user_change.go`, `user_delete.go`, `user_get.go`, `user_list.go`, `user_token.go`, `user_totp.go` | `docs/docs/reference/cli/users.md` |\n| `project.go`, `project_export.go`, `project_import.go` | `docs/docs/reference/cli/projects.md` |\n| `vault.go`, `vault_check.go`, `vault_rekey.go` | `docs/docs/reference/cli/vaults.md` |\n| `runner.go`, `runner_register.go`, `runner_setup.go`, `runner_start.go`, `runner_unregister.go` | `docs/docs/reference/cli/runners.md` |\n| `migrate.go` | `docs/docs/reference/cli/migrations.md` |\n\n`*_test.go` files (e.g. `runner_register_test.go`, `user_token_test.go`) encode exact behavior — read them to\ndocument precise rules (e.g. registration-token resolution order, token expiry formatting).\n\n`syslog.go` / `syslog_windows.go` are internal helpers, **not** commands — never document them.\n\nRe-discover the command tree before each run; don't trust this table if files were added/removed:\n\n```bash\nls cli/cmd/*.go | grep -v _test\ngrep -rn \"cobra.Command{\" cli/cmd/ | grep -v _test          # every command/subcommand\ngrep -rn \"AddCommand\\|PersistentFlags()\\.\" cli/cmd/          # wiring + flags\n```\n\n## How to read a Cobra command\n\nExtract these facts mechanically from each `*Cmd = &cobra.Command{...}` and its `init()`:\n\n- **Command name** — the `Use:` field (first word). Subcommands are wired with `parent.AddCommand(child)`.\n- **Aliases** — the `Aliases:` slice. Document them (e.g. `users`/`user`, `projects`/`project`,\n  `vaults`/`vault`, `server`/`service`). A command whose `Run` only calls `cmd.Help()` is a pure group — it has\n  no behavior of its own, just subcommands.\n- **Flags** — each `cmd.PersistentFlags().<Type>Var(&arg, \"name\", default, \"usage\")` becomes one table row:\n  flag = `--name`, description from the usage string, and note the default when it's meaningful (e.g.\n  `--enabled` defaults to `true`, `--project-id` defaults to `0` = \"global\"). `StringSliceVar` → \"comma-separated\n  or repeat the flag\".\n- **Required / mutually-exclusive args** — read the `Run` body. Patterns like\n  `if args.x == \"\" { fmt.Println(\"Argument --x required\"); ok = false }` mean **required**; checks that reject\n  setting both of two flags mean **exactly one of**. Document these rules explicitly (\"Exactly one of `--a` or\n  `--b` is required\").\n- **Output** — what the command prints on success (IDs, a table, nothing). Mirror the real `fmt.Printf` format\n  in the doc, including example output blocks for commands that print structured results (e.g. `vault check`).\n\n### Global flags (from `root.go`)\n\nDocumented once in `cli.md`; they apply to every command. Currently: `--config`, `--no-config`, `--log-level`\n(`DEBUG|INFO|WARN|ERROR|FATAL|PANIC`, env `SEMAPHORE_LOG_LEVEL`), `--debug-filter` (DEBUG-only namespace filter,\nenv `SEMAPHORE_DEBUG_FILTER`). Re-read `Execute()` in `root.go` for the live list.\n\n## Page structure (match the existing style)\n\nEvery command-group page follows the shape established in `users.md` and `vaults.md`:\n\n1. `# <Title>` then a one-sentence purpose.\n2. A fenced `semaphore <group> --help` block.\n3. An alias note as a `>` blockquote when the group has an alias (`> \\`vault\\` is an alias for \\`vaults\\`.`).\n4. A subcommand table (`| Command | Purpose |`) with in-page anchor links when the page is long.\n5. One `##` section per subcommand: a short description, a copy-pasteable example, then a\n   `| Flag | Description |` table. State required/exactly-one rules in prose under the table.\n\nConventions:\n\n- Internal links are absolute from the route base `/`: **`/reference/cli/<page>`** (NOT `/cli/<page>` — that\n  base is wrong and 404s). Cross-links to concept pages use the same form, e.g. `/admin-guide/runners`,\n  `/admin-guide/security/encryption`.\n- Admonitions use Docusaurus syntax: `:::tip`, `:::warning` … `:::`.\n- Keep prose task-oriented and match the tone/heading depth of neighboring pages.\n\n## Index page (`cli.md`) and sidebar\n\n- `cli.md` carries: intro + `semaphore help`, a command-group table linking to each page, the **global options**\n  table, and short sections for `version`, `setup`, `server`, `runner`, `migrate`.\n- Every command-group page must be registered in **`docs/sidebars.js`** under the `label: 'CLI'` category's\n  `items` array, as `'reference/cli/<page>'`. Adding a page without registering it hides it from navigation.\n\n## Workflow\n\n### 1. Enumerate the live command tree\nRun the discovery greps above. Build the set of command groups and subcommands that actually exist. Note any\ngroup/subcommand/flag that has no corresponding doc section, and any doc section with no corresponding code.\n\n### 2. Extract facts per command\nFor each command, pull name, aliases, flags (name/default/usage), required/exclusive rules (from `Run`), and\nsuccess output. Cross-check `*_test.go` for exact behavior.\n\n### 3. Update each page\nRewrite or patch each page so it matches the extracted facts and the page structure above. Add sections for new\nsubcommands; remove sections for deleted ones; fix flag tables. Leave a page untouched only after confirming it\nalready matches the code.\n\n### 4. Update the index and sidebar\nRefresh `cli.md` (command table, global options, operational commands) and ensure every page is listed in\n`docs/sidebars.js`.\n\n### 5. Build to catch broken links\nFrom the `docs/` directory:\n\n```bash\ncd docs && npm run build 2>&1 | tail -40\n```\n\nThe build must end with `[SUCCESS] Generated static files`. **Broken-link** errors in pages you touched must be\nfixed. Pre-existing **broken-anchor** warnings in unrelated pages (e.g. `admin-guide/configuration`,\n`admin-guide/installation_manually`) are not yours to fix — note them and move on. If `node_modules` is absent,\nrun `npm install` first (or tell the user the build couldn't run).\n\n## Pitfalls\n\n- **Dead flags.** A flag registered in `init()` but never read in the command's `Run` does nothing — do not\n  document it as functional. Example: after BoltDB removal, `migrate` still registers `--err-log-size`,\n  `--skip-task-output`, `--merge-existing-users`, but `Run` only uses `--apply-to` / `--undo-to`. Verify each\n  flag is actually consumed before documenting it.\n- **Removed features → version-gate, don't silently delete.** When a command/flag is removed (e.g.\n  `migrate --from-boltdb`; BoltDB is \"not supported starting from 2.19\" per `cli/setup/setup.go`), keep the\n  guidance but mark the version boundary with a `:::warning`, so users on older versions are still served. Note\n  when other parts of the repo (e.g. `deployment/docker/server/server-wrapper`) still reference a removed flag —\n  that's a stale call site worth flagging to the user, but out of scope for the docs change.\n- **Stale help strings.** A command's `Short`/`Long` text can lag the code. Verify config-key names against the\n  actual structs in `util/` (e.g. `vault rekey`'s `--old-key` help says `access_key`, but the real config field\n  is `secret_key` / `secret_key_file` in `util/config.go`). Document the struct, not the help string.\n- **Group commands have no flags of their own.** `users`, `projects`, `vaults`, `runner`, `token`, `totp` just\n  print help and dispatch to subcommands. Don't invent flags for them.\n- **Don't document the internal `server` runtime.** `server.go`/`root.go` wire a lot of services; the doc only\n  needs \"start the server, `service` is an alias, respects global flags.\"\n\n## When to ask or refuse\n\n- If a command's behavior is genuinely ambiguous from the code (e.g. a flag is read but its effect spans\n  packages you can't quickly trace), summarize what you found and ask the user rather than guessing.\n- If asked to document a flag or command that **doesn't exist in `cli/cmd/`**, refuse and explain: CLI docs are\n  generated from the Cobra code — add the command/flag first, then regenerate.\n- Before deleting a whole documented section (a removed command group), propose it explicitly; prefer\n  version-gating over silent removal when older releases still ship the feature.\n",".claude/skills/semaphore-commit/SKILL.md":"---\nname: semaphore-commit\n---\n\nCreate well-formatted git commits following conventional commit standards.\n\n## Behavior\n1. Analyze staged changes with `git diff --staged`\n2. Generate a conventional commit message\n3. Create the commit with proper formatting\n4. Do not add \"Co-Authored-By: Claude ... <noreply@anthropic.com> to the footer\"\n\n## Commit Format\n```\n<type>(<scope>): <description>\n\n[optional body]\n\n[optional footer]\n```\n\n## Types\n- feat: New feature\n- fix: Bug fix\n- docs: Documentation changes\n- style: Code style changes\n- refactor: Code refactoring\n- test: Adding or modifying tests\n- chore: Maintenance tasks\n- agents: Changes related to AI agents work\n\n## Example Output\n```\nfeat(auth): add password reset functionality\n\n- Add forgot password form\n- Implement email verification flow\n- Add password reset endpoint\n```\n",".claude/skills/semaphore-config-schema/SKILL.md":"---\nname: semaphore-config-schema\ndescription: >\n  Generate or update the JSON Schema (in YAML form) that describes the Semaphore configuration file. The schema\n  lives at `config.schema.yaml` in the repository root and is derived from the Go struct `util.ConfigType`\n  (with nested types in `util/config.go`, `util/config_auth.go`, `util/App.go`, `util/OdbcProvider.go`). Use this\n  skill whenever the user asks to \"regenerate the config schema\", \"update config.schema.yaml\", \"sync the schema\n  with ConfigType\", \"add a new field to the schema\", \"the schema is out of date\", or after a Go struct change\n  that affects configuration. Also trigger when the user adds a new top-level field, new nested struct, or\n  changes a `json:`, `default:`, `rule:`, or `env:` tag on a config struct field.\n---\n\n# Semaphore — Config Schema Generator\n\nThis skill produces and maintains `config.schema.yaml` — a **JSON Schema draft 2020-12** document (written in\nYAML) describing every valid key in a Semaphore `config.json` / `config.yaml`. The schema is the source of truth\nfor IDE autocomplete (via `yaml-language-server`) and for any future server-side config validation.\n\nThe schema is **derived from Go code**, never hand-edited speculatively. If `ConfigType` doesn't have a field,\nthe schema must not have it. If a field has a `default:` or `rule:` tag, the schema must reflect it.\n\n## Inputs (source of truth)\n\nAll Go types live in the `util` package. Read these files **in full** before writing anything:\n\n| File | What's in it |\n|------|--------------|\n| `util/config.go` | `ConfigType` (root) + most nested types: `DbConfig`, `TLSConfig`, `RunnerConfig`, `TotpConfig`, `EventLogType`, `TaskLogType`, `ConfigLog`, `SyslogConfig`, `ConfigProcess`, `ConfigAppNamespaces`, `ScheduleConfig`, `DebuggingConfig`, `HAConfig`, `HARedisConfig`, `TeamsConfig`, `ConfigDirs`, `SubscriptionConfig`, `LdapMappings`, `oidcEndpoint` |\n| `util/config_auth.go` | `MultifactorAuthConfig`, `EmailAuthConfig`, `TotpConfig`, `RecaptchaConfig` |\n| `util/App.go` | `App` |\n| `util/OdbcProvider.go` | `OidcProvider` (note: filename is `Odbc...`, typo'd — don't \"fix\" it) |\n\nIf a struct moves or a new file appears, find it with:\n\n```bash\ngrep -rn \"^type .* struct\" util/ | grep -v _test\n```\n\n## Output\n\nA single file: **`config.schema.yaml`** in the repository root.\n\nHeader (top of file) — keep exactly this shape:\n\n```yaml\n$schema: \"https://json-schema.org/draft/2020-12/schema\"\n$id: \"https://semaphoreui.com/schemas/config.schema.json\"\ntitle: Semaphore configuration\ndescription: Schema for Semaphore UI config.json / config.yaml. Generated from util.ConfigType.\ntype: object\n```\n\n## Field-mapping rules\n\nThese are non-negotiable — apply them mechanically.\n\n### Property name\n\nUse the **`json:` tag's first segment** (before any comma):\n\n```go\nPort string `json:\"port,omitempty\" ...`   // → property name \"port\"\n```\n\nSkip fields where the `json:` tag is `\"-\"` (e.g. `RunnerConfig.RegistrationToken`, `SubscriptionConfig.Key` —\nthose are env-only, not file-configurable). Document this in the schema's `description` for the containing\nstruct if a sensitive token is reachable only via env.\n\nSkip unexported fields (lowercase first letter) — but include their **referenced types** if they appear via\nexported fields. Example: `oidcEndpoint` is unexported but referenced from `OidcProvider.Endpoint`, so define\nit under `$defs` as `OidcEndpoint`.\n\n### JSON Schema `type`\n\n| Go type | Schema type |\n|---------|-------------|\n| `string` | `string` |\n| `bool` | `boolean` |\n| `int`, `int64`, `uint32` | `integer` |\n| `float64` | `number` |\n| `[]T` | `array`, with `items: { ... }` |\n| `map[string]T` | `object`, with `additionalProperties: { ... }` |\n| `*T` (pointer) | same as `T`. If `*int`, allow `null`: `type: [integer, \"null\"]` |\n| `*Struct` | `{ $ref: \"#/$defs/Struct\" }` |\n\n### Constraints from tags\n\n- **`default:\"X\"`** → emit `default: X` (cast to the right type — `default:\"10\"` on `int` becomes `default: 10`).\n- **`rule:\"^...$\"`** → emit `pattern: \"^...$\"`. **Don't** also add a `format:` — the regex is more authoritative.\n- **`env:\"NAME,sensitive\"`** → append `(sensitive).` to the `description`. The schema doesn't have a native\n  \"sensitive\" flag, so this is documentation-only.\n- **`env:\"NAME\"`** without `sensitive` → no change to schema.\n- URL-looking string fields (web hooks, redirect URLs) → add `format: uri` **unless** a `pattern` is already\n  set from a `rule:` tag.\n\n### `enum` discovery\n\n`enum` is not always in tags — sometimes it's in code:\n\n1. Check for a `rule:\"^a|b|c$\"` regex — convert that to `enum: [a, b, c]` instead of a pattern (more useful in\n   IDE).\n2. Check for typed constants near the struct (e.g. `SyslogFormat` has `SyslogDefault = \"\"` and `SyslogRFC5424\n   = \"rfc5424\"` → `enum: [\"\", rfc5424]`).\n3. `TeamInviteType` constants (`TeamInviteEmail`, `TeamInviteUsername`, `TeamInviteBoth`) → enum.\n\n### Reusable types → `$defs`\n\nEvery named struct that appears as a field type gets a `$defs/<TypeName>` entry. Reference via `$ref:\n\"#/$defs/<TypeName>\"`. Top-level `properties` should reference, not inline.\n\nException: `lumberjack.Logger` is external — define a minimal stub `$defs/LumberjackLogger` with the common\nfields (`filename`, `maxsize`, `maxage`, `maxbackups`, `localtime`, `compress`). Do not try to fully model it.\n\n### `additionalProperties`\n\nLeave it **unset** (defaults to `true`). The schema is informational, not strict — making it strict would\nbreak existing configs that have stray keys (legacy fields, typos used as comments, etc.).\n\n## Workflow\n\n### 1. Read all four Go files\n\nUse `Read` on each file. Do not skim. Note every exported field on every struct.\n\n### 2. Build a mental map: struct → properties\n\nFor `ConfigType` (the root), list every exported field with its `json:` name, Go type, and any `default:` /\n`rule:` / `env:` tags. Then for each referenced struct, do the same. Stop when no new structs appear.\n\n### 3. Diff against existing `config.schema.yaml`\n\nBefore writing, compare what Go has vs. what the schema has:\n\n```bash\n# List all json tag names in Go\ngrep -hoE 'json:\"[^,\"]+' util/*.go | sed 's/json:\"//' | sort -u > /tmp/go-keys.txt\n\n# List all property keys in the schema\nyq '.properties | keys | .[]' config.schema.yaml | sort -u > /tmp/schema-keys.txt\n\ndiff /tmp/go-keys.txt /tmp/schema-keys.txt\n```\n\nIdentify three categories:\n- **In Go, missing from schema** → add.\n- **In schema, missing from Go** → remove (the Go struct is the source of truth).\n- **In both** → check that type / default / pattern still match.\n\nShow the user the diff before applying changes if it's larger than ~5 lines.\n\n### 4. Write `config.schema.yaml`\n\nUse the **YAML formatting conventions** already established in the file:\n\n- Section comments split top-level `properties` into visual groups:\n  `# === Database ===`, `# === HTTP server ===`, `# === Authentication ===`, etc.\n- Inline-object syntax `{ type: string, default: foo }` for one-property entries.\n- Multi-line block form when there are 3+ properties or a `description`.\n- Quote string values that contain `:`, `/`, `+`, `=`, or start with a digit. Don't quote bare identifiers\n  like `string`, `boolean`, `integer`.\n- `$ref` values are quoted: `{ $ref: \"#/$defs/DbConfig\" }`.\n\n### 5. Validate\n\nAfter writing, run two checks:\n\n```bash\npython3 - <<'PY'\nimport yaml, jsonschema\nwith open(\"config.schema.yaml\") as f:\n    schema = yaml.safe_load(f)\njsonschema.Draft202012Validator.check_schema(schema)  # meta-validation\nprint(\"schema is valid\")\n\n# Confirm an existing config.yaml still passes\ntry:\n    with open(\"config.yaml\") as f:\n        cfg = yaml.safe_load(f)\n    errors = list(jsonschema.Draft202012Validator(schema).iter_errors(cfg))\n    print(f\"config.yaml: {len(errors)} error(s)\")\n    for e in errors:\n        print(f\"  {list(e.path)}: {e.message}\")\nexcept FileNotFoundError:\n    pass\nPY\n```\n\nBoth must pass. If `config.yaml` doesn't exist locally, skip the second check — the meta-validation alone is\nsufficient.\n\n### 6. Don't touch `config.yaml`\n\nThe user's `config.yaml` may contain stray legacy keys or comments. Do not \"clean it up\" as part of this\nskill — the schema generation is a one-way operation Go → schema. If the user wants a cleaner config, that's a\nseparate task.\n\n## Common pitfalls\n\n- **`json:\"-\"` fields**: easy to accidentally include. Always check the tag, not the field name.\n- **Pointer vs. value**: `*int` is nullable, plain `int` is not. Reflect that in `type`.\n- **Typo'd filenames**: `util/OdbcProvider.go` (should be `Oidc`) — preserve it; renaming files is out of scope.\n- **Tag inheritance illusion**: nested structs don't inherit tags from their parent field. The `default:` on\n  `ConfigType.MaxParallelTasks` is on the field, not on `int`.\n- **`sensitive` is not standard**: JSON Schema has no built-in sensitive marker. Encode it in `description`\n  text only.\n- **Existing keys with apostrophes** in `config.yaml` (e.g. `\"'email_sender\"`) are user-side comment hacks —\n  ignore them, they're not in any Go struct.\n\n## When to refuse\n\nIf the user asks to add a property to the schema that **doesn't exist in `ConfigType`** (\"just add a `foo`\nfield so my config validates\"), refuse and explain: the schema is generated from Go. The fix is to add the\nfield to the Go struct first, then regenerate.\n\nIf `ConfigType` has been heavily refactored and a previous schema entry no longer maps, propose the deletion\nexplicitly before doing it — never silently drop entries that the user might have been relying on.\n",".claude/skills/semaphore-third-party-licenses/SKILL.md":"---\nname: semaphore-third-party-licenses\ndescription: >\n  Generate or update the THIRD-PARTY-LICENSES.md file for the Semaphore UI repository. Use this skill whenever the user \n  asks to create, update, regenerate, audit, or refresh third-party license attributions, OSS notices, SBOM-style \n  license inventories, or NOTICE files for Semaphore UI. Trigger on phrases like \"third-party licenses\", \"OSS notices\",\n  \"license attribution file\", \"SBOM for licenses\", \"NOTICE file\", \"audit dependencies\",\n  \"licenses for go.mod / package.json\", or any request that mentions compliance with customer agreements requiring an\n  OSS components list. Also trigger when the user says \"we added a new dependency, regenerate licenses\". This skill\n  knows the Semaphore UI layout (Go backend + Vue.js frontend), the project's license policy (allowlist/denylist),\n  and the canonical output format.\n---\n\n# Semaphore UI — Third-Party License Generator\n\nThis skill produces `THIRD-PARTY-LICENSES.md` for the Semaphore UI repository. The file lists every open-source\ndependency shipped with Semaphore UI by **name**, **version**, **license**, and **source URL**, grouped by component. It\nexists to satisfy contractual obligations toward customers (identification of OSS components by name, version,\nand license type) and to preserve attribution required by permissive licenses (MIT/BSD/Apache-2.0).\n\n## Repository layout you can expect\n\nSemaphore UI is a self-hosted product. The repository ships:\n\n- **Go backend** — module file at `go.mod`, source under `pkg/`, `services/`, `db/`, etc.\n- **Vue.js frontend** — `web/package.json` (or `web2/package.json` in some branches), built into static assets and\n  embedded in the Go binary.\n\nExternal CLI tools (Ansible, Terraform, OpenTofu, Pulumi, Bash) are invoked at runtime as separate operating-system\nprocesses. They are NOT linked into the binary and NOT redistributed with Semaphore UI, so they are intentionally\n**not listed** in `THIRD-PARTY-LICENSES.md` — their license terms apply to the user who installs them on the host,\nnot to Semaphore UI's distribution. The license policy still discusses why this is contractually safe (see\n`references/license_policy.md`, \"Subprocess exception\").\n\nIf the layout differs (monorepo restructure, new submodule, etc.), inspect the repo first with\n`find . -name \"go.mod\" -o -name \"package.json\" -not -path \"*/node_modules/*\"` and adapt.\n\n## Workflow\n\nFollow these steps in order. Don't skip the policy check — it's the part that actually protects the project from\naccidentally shipping a GPL'd dependency.\n\n### 1. Confirm scope with the user\n\nBefore generating anything, ask (briefly) which targets to include if it's not obvious from context:\n\n- Backend only? Frontend only? Both?\n- Production dependencies only, or also dev dependencies?\n\n**Default**: both backend and frontend, **production-only** (dev dependencies are not distributed to customers and don't\nneed attribution). Override only if the user explicitly asks for dev deps too.\n\n### 2. Collect raw license data\n\nRun `scripts/collect_licenses.sh` from the repo root. It does the following:\n\n- Sets `GOWORK=off` and runs `go-licenses report ./... --template scripts/go_template.tpl` for the Go backend, so\n  workspace siblings (e.g. `pro_impl`) are not pulled in.\n- Rolls sub-package rows up to their parent module (uses `go list -m all` for the module → version map) and drops\n  any module path that belongs to the root module's namespace (first-party packages, including `replace … => ./pro`\n  targets).\n- Applies a small `OVERRIDES` table in the script for modules where `go-licenses` can't auto-detect a license\n  (e.g. `modernc.org/mathutil` → BSD-3-Clause). Extend the table when new \"Unknown\" entries appear.\n- Runs `license-checker --production --json --excludePrivatePackages` in the frontend directory and strips the\n  low-confidence `*` suffix from license strings so `MIT*` collapses into `MIT`.\n- Writes raw output to `.licenses-cache/go.tsv` and `.licenses-cache/npm.json`.\n\nIf the tools aren't installed, the script prints the install commands. Don't try to install them silently — show the\nuser what needs to be added so they can decide whether to add them to CI.\n\n### 3. Run the policy check\n\nRun `scripts/check_policy.py .licenses-cache/`. This compares every detected license against the project policy in\n`references/license_policy.md`:\n\n- **Allowed** (green): MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Zlib, Unlicense, CC0-1.0, MPL-2.0 — proceed.\n- **Review** (yellow): LGPL-2.1, LGPL-3.0, EPL-2.0, CDDL-1.0, custom/unknown — pause and ask the user. LGPL is fine for\n  dynamic linking but Go statically links, so flag it for review.\n- **Forbidden** (red): GPL-2.0, GPL-3.0, AGPL-3.0, SSPL-1.0, BUSL-1.1, Commons Clause, \"non-commercial\" clauses — STOP.\n  Do not proceed with file generation. Report the offending package and ask the user how to handle it (replace, vendor,\n  or accept and document the consequence).\n\n**This step is non-negotiable.** A forbidden license slipping into THIRD-PARTY-LICENSES.md isn't just a documentation\nproblem — it's a contract breach with every customer. If `check_policy.py` returns a  non-zero exit code, surface the \nfailures prominently and stop.\n\n### 4. Generate the markdown\n\nRun `scripts/generate_md.py .licenses-cache/ > THIRD-PARTY-LICENSES.md`. This produces the file using the structure\ndescribed in `references/template.md`. Key points the generator handles:\n\n- Sorts entries alphabetically within each section.\n- Groups by component category (Go backend / npm frontend).\n- Deduplicates packages that appear at multiple versions (lists all versions on one line).\n- Renders a summary table at the top with license counts.\n- Adds a generation timestamp and a note pointing to `scripts/collect_licenses.sh` so the file is reproducible.\n\n### 5. Verify and commit\n\nAfter generation:\n\n1. Diff against the previous `THIRD-PARTY-LICENSES.md` (if it exists). New rows = new dependencies — confirm with the\n   user that they're expected.\n2. Verify the file isn't truncated and ends with the closing footer (`<!-- end of generated file -->`).\n3. Suggest the commit message: `chore(licenses): regenerate THIRD-PARTY-LICENSES.md for vX.Y.Z`.\n4. If a CI pipeline exists (`.github/workflows/`), suggest adding a job that runs `scripts/check_policy.py` on every\n   PR — that's how the project prevents regressions, not by re-checking manually each release.\n\n## Notes on edge cases\n\n**Vendored dependencies.** If the repo has a `vendor/` directory, `go-licenses` will read from it. That's fine —\nvendored code still ships and still needs attribution.\n\n**Dual-licensed packages.** Some packages are offered under \"MIT OR Apache-2.0\". Pick MIT (the simpler one) and note the\nchoice in the entry: `License: MIT (also available under Apache-2.0)`.\n\n**Packages with no detectable license.** `go-licenses` flags these as \"Unknown\". Don't ship the file with Unknowns —\nmanually inspect the repo, fill in the SPDX identifier, and if there's truly no license, treat the package as\nforbidden (no license = \"all rights reserved\", which is incompatible with redistribution).\n\n**Ansible / Terraform / OpenTofu / Pulumi / Bash.** Do NOT list them in `THIRD-PARTY-LICENSES.md`. They are invoked\nas separate OS processes (`os/exec`), not linked, not redistributed. Their licenses bind the user who installs them\non the host, not Semaphore UI's distribution. Listing them would imply otherwise and is an unnecessary signal to a\nreviewing customer's legal team. The \"Subprocess exception\" section in `references/license_policy.md` explains the\nreasoning if a reviewer asks.\n\n**First-party packages with no LICENSE.** The root module and any `replace … => ./<dir>` target (e.g. `./pro`) live\ninside this repo and don't need to be self-attributed. The collection script filters them out by the root module\nprefix from `go.mod`. If you see \"Unknown license\" rows for `github.com/<this-org>/<this-repo>/…`, the filter is\nworking correctly — those rows shouldn't appear in the final cache.\n\n**Embedded license texts.** For packages under MIT/BSD/ISC, the license requires preserving the copyright notice. The\ngenerator embeds the full LICENSE file text from each package's source. For Apache-2.0 packages, it embeds the NOTICE\nfile if one exists.\n\n## What this skill does NOT do\n\n- It does not generate a CycloneDX or SPDX SBOM. If the user wants a machine-readable SBOM (for compliance scanners,\n  customer attestations, etc.), point them at `syft packages dir:. -o cyclonedx-json` as a separate step.\n  THIRD-PARTY-LICENSES.md is the human-readable artifact.\n- It does not check for CVEs. That's a security concern, not a licensing one — use `govulncheck` and `npm audit` for\n  that.\n- It does not modify go.mod or package.json. The skill is read-only with respect to the dependency manifests; it only\n  generates the attribution document.\n\nSee `references/license_policy.md` for the full policy and `references/template.md` for the output format.\n",".claude/skills/semaphore-third-party-licenses/references/license_policy.md":"# License Policy for Semaphore UI\n\nThis is the source of truth for which open-source licenses are acceptable in\nSemaphore UI dependencies. The policy is enforced by\n`scripts/check_policy.py` — if you change anything here, mirror the change\nin that script.\n\nThe policy exists for two reasons:\n\n1. **Customer obligations.** Our MSA commits us to *not* shipping\n   components on terms that would require customers to license or assign\n   their IP. That rules out copyleft licenses with strong reciprocity\n   (GPL family, AGPL, SSPL).\n2. **Project sustainability.** Source-available-but-not-OSS licenses\n   (BUSL, Commons Clause, Elastic 2.0) carry use restrictions that conflict\n   with our open-source distribution model.\n\n## Allowed (no review needed)\n\nThese are permissive licenses with attribution-only obligations. Anything\nunder one of these can be added without further discussion.\n\n| SPDX ID | Name |\n|---------|------|\n| MIT, MIT-0 | MIT License |\n| Apache-2.0 | Apache License 2.0 |\n| BSD-2-Clause | BSD 2-Clause \"Simplified\" |\n| BSD-3-Clause | BSD 3-Clause \"New\" |\n| ISC | ISC License |\n| Zlib | zlib License |\n| Unlicense | The Unlicense |\n| CC0-1.0 | Creative Commons Zero |\n| MPL-2.0 | Mozilla Public License 2.0 (file-level copyleft, OK for libraries) |\n| 0BSD | BSD Zero Clause |\n| BlueOak-1.0.0 | Blue Oak Model License |\n| Python-2.0 | Python Software Foundation License |\n\n**Note on MPL-2.0:** MPL is allowed because its copyleft obligation is at\nthe *file* level, not the project level. We can use MPL-licensed libraries\nwithout making Semaphore UI MPL. If we ever modify an MPL-licensed file,\nthat file must remain MPL.\n\n## Review required\n\nThese licenses are not blanket-forbidden but require a case-by-case\ndecision. Pause and ask before merging a dependency under one of these.\n\n| SPDX ID | Why review |\n|---------|------------|\n| LGPL-2.1, LGPL-3.0 (and `-or-later`) | LGPL is fine for **dynamic** linking, but Go statically links by default. An LGPL Go library would force us to provide object files so users can relink. Acceptable only if the library is genuinely irreplaceable and we can document the relinking pathway. |\n| EPL-1.0, EPL-2.0 | Eclipse Public License has weak copyleft and an explicit patent clause. Generally workable but worth a legal eyeball. |\n| CDDL-1.0, CDDL-1.1 | File-level copyleft like MPL, but with extra requirements. Rare in our ecosystem. |\n| EUPL-1.1, EUPL-1.2 | European Union Public License. Compatible with several other licenses; check the specific version. |\n| OFL-1.1 | SIL Open Font License. Almost always fine for embedded fonts, but verify the font is being used as intended (not redistributed standalone with a new name). |\n\n## Forbidden\n\nA dependency under one of these licenses **must not be added**. If\n`check_policy.py` flags one, the options are: (a) remove it, (b) replace\nit with a permissively-licensed alternative, (c) move the functionality\nout of process (subprocess invocation breaks the linkage in most cases —\nthis is why Ansible is acceptable).\n\n| SPDX ID | Why forbidden |\n|---------|--------------|\n| GPL-2.0, GPL-3.0 (and `-only` / `-or-later`) | Strong copyleft. Linking forces Semaphore UI to be GPL, which conflicts with our distribution model. |\n| AGPL-1.0, AGPL-3.0 | Same as GPL plus a network-interaction trigger. Particularly hostile to SaaS/self-hosted server software. |\n| SSPL-1.0 | MongoDB's Server Side Public License. Not OSI-approved; imposes obligations on anyone offering the software as a service. |\n| BUSL-1.1 | Business Source License. Time-delayed open source with use restrictions during the change window. Not OSS. |\n| Commons-Clause | Restricts commercial sale; strips OSS status from any underlying license it's attached to. |\n| Elastic-2.0 | Elastic License v2. Use restrictions; not OSS. |\n| RSALv2 | Redis Source Available License v2. Same family as BUSL/Elastic. |\n| CC-BY-NC-* | Creative Commons \"Non-Commercial\" variants. Not usable in a commercially-distributable product. |\n| Facebook-BSD-Patents (legacy) | The old React patent grant clause. Removed years ago, but flagged here as a defensive measure. |\n\n## Subprocess exception (External Runtime Tools)\n\nTools that Semaphore UI invokes via `os/exec` (Ansible, Terraform, OpenTofu,\nPulumi, Bash) are NOT linked into our binary and NOT redistributed by us.\nTheir licenses govern the user's installation of those tools on the host\nsystem, not our distribution.\n\nThis is why we can integrate with Ansible (GPL-3.0) and Terraform (BUSL-1.1)\nwithout violating this policy — we never embed their code, we only document\nthat the user must install them separately.\n\nThese tools are intentionally **not listed** in `THIRD-PARTY-LICENSES.md`.\nListing them would wrongly imply that they are distributed with the product\nand would invite unnecessary scrutiny from a customer's legal team. Neither\n`collect_licenses.sh` nor `generate_md.py` produces or accepts an external-\ntools section.\n\n## When the policy itself needs to change\n\nIf a dependency we genuinely need is forbidden, the path forward is:\n\n1. Document the technical need (what does it do, what alternatives exist).\n2. Get sign-off from a maintainer with the authority to make that call.\n3. Update **both** this document and `check_policy.py`, in the same commit.\n4. Note the exception in `THIRD-PARTY-LICENSES.md` so customers can see it.\n\nDon't relax the policy silently. The whole point is that customers can\naudit us, so the policy must be auditable too.\n",".claude/skills/semaphore-third-party-licenses/references/template.md":"# THIRD-PARTY-LICENSES.md output format\n\nThis is the canonical structure that `scripts/generate_md.py` produces.\nReference for humans reviewing the generator output, and for anyone who\nneeds to understand what each section is for.\n\n## Structure\n\n```\n# Third-Party Licenses\n\n[intro paragraph explaining what this file is and why it exists]\n\n_Generated on YYYY-MM-DD HH:MM UTC by scripts/collect_licenses.sh._\n\n[regeneration instructions — always include these so the file is\n reproducible by anyone, not just the original author]\n\n## Summary\n\n[total component count]\n\n| Ecosystem | Components |\n|-----------|------------|\n| Go (backend)        | NN |\n| npm (frontend)      | NN |\n\n### License distribution\n\n| License | Count |\n|---------|-------|\n| MIT          | NN |\n| Apache-2.0   | NN |\n| BSD-3-Clause | NN |\n| ...          | NN |\n\n## Go Backend Dependencies\n\n[one-line description of what these are and where they come from]\n\n| Component | Version(s) | License | Source |\n|-----------|------------|---------|--------|\n| `github.com/foo/bar`  | v1.2.3 | MIT | [link](https://...) |\n| `github.com/foo/baz`  | v0.4.1, v0.5.0 | Apache-2.0 | [link](https://...) |\n| ...\n\n## Frontend Dependencies (npm)\n\n[one-line description]\n\n| Component | Version(s) | License | Source |\n|-----------|------------|---------|--------|\n| `vue`        | 3.4.21 | MIT | [link](https://...) |\n| `vuetify`    | 3.5.8  | MIT | [link](https://...) |\n| ...\n\n---\n\n## License texts\n\n[paragraph explaining where to find full license texts and how to report\n missing/incorrect attributions]\n\n<!-- end of generated file -->\n```\n\n## Why this structure\n\n**Summary table at the top.** Lets a reader (especially a customer's\ncompliance team) see the shape of the dependency tree at a glance\nwithout scrolling through hundreds of rows.\n\n**License distribution.** Surfaces concentration risk. If 95% of deps are\nMIT/Apache and 1 is MPL-2.0, that 1 deserves attention. It also makes it\neasy to spot a forbidden license that somehow slipped through (a single\nGPL-3.0 row in the distribution table is a very loud red flag).\n\n**Sorted alphabetically within each section.** Diffs between releases\nbecome useful — a new dependency shows up as an added row in one\npredictable place, not scattered across the file.\n\n**Versions collapsed onto one line.** A dependency pinned at multiple\nversions (common with transitive Go deps that get MVS-resolved differently\nin different parts of the tree) shouldn't take 5 rows. One row, comma-\nseparated versions.\n\n**External tools deliberately excluded.** Ansible, Terraform, OpenTofu,\nPulumi, and Bash are invoked as subprocesses, not linked, and not\nredistributed with Semaphore UI. They are not listed in this file —\nincluding them would wrongly imply distribution. The license policy\ndocument covers them under \"Subprocess exception\" if a reviewer asks\nwhy they're absent.\n\n**Regeneration instructions in the header.** Means the file is not a\nhand-maintained artifact that drifts. Anyone can rebuild it and verify\nit matches what's actually in the dependency manifests.\n\n**Footer comment marker.** `<!-- end of generated file -->` lets CI\nverify the file wasn't truncated mid-write — useful when generation runs\nin a pipeline and might fail partway.\n\n## What NOT to include\n\n- **Full license texts inline.** Don't copy the MIT license text 200\n  times into one file. Refer readers to the source URL. The packages\n  themselves carry their LICENSE files; the user has them on disk after\n  `go mod download` / `npm install`.\n- **CVE / vulnerability info.** That belongs in security advisories,\n  not the license document. Mixing the two confuses the reader and makes\n  the file go stale faster.\n- **Internal-only dependencies.** Build tooling (linters, test\n  frameworks, code generators) doesn't ship to customers. Production\n  dependencies only.\n- **Hand-edited rows.** If something needs a special note, add it via\n  the generator (extend `generate_md.py`), not by editing the output\n  file. Hand edits get destroyed on the next regeneration.\n",".github/copilot-instructions.md":"# Semaphore UI Development Instructions\n\n**Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**\n\nSemaphore UI is a modern web interface for managing popular DevOps tools like Ansible, Terraform, PowerShell, and Bash scripts. It's built with Go (backend) and Vue.js (frontend), using Task runner for build automation.\n\n## Working Effectively\n\n### Bootstrap, build, and test the repository:\n- Install Go 1.21+ (currently requires go version 1.21 or higher)\n- Install Node.js 16+ \n- Install Task runner: `go install github.com/go-task/task/v3/cmd/task@latest`\n- Install dependencies: `task deps` -- takes 3 minutes first time (faster with cache). NEVER CANCEL. Set timeout to 5+ minutes.\n- Build the application: `task build` -- takes 1.5 minutes. NEVER CANCEL. Set timeout to 3+ minutes.\n- Run tests: `task test` -- takes 33 seconds. NEVER CANCEL. Set timeout to 2+ minutes.\n\n### Run the application:\n- ALWAYS run the bootstrapping steps first\n- Setup database and admin user: `./bin/semaphore setup` (interactive, use SQLite option 4 for development)\n- Start server: `./bin/semaphore server --config ./config.json`\n- Web UI: http://localhost:3000 (login: admin / changeme)\n- API: http://localhost:3000/api/ (test with: `curl http://localhost:3000/api/ping`)\n\n## Validation\n\n- **CRITICAL**: Always manually validate any new code by building and running the application.\n- ALWAYS run through at least one complete end-to-end scenario after making changes:\n  1. Build the application: `task build`\n  2. Start the server: `./bin/semaphore server --config ./config.json`\n  3. Test API endpoint: `curl http://localhost:3000/api/ping` (should return \"pong\")\n  4. Access web UI at http://localhost:3000 and verify it loads\n  5. For auth changes: Test login with admin/changeme\n- For significant changes, run full setup process to ensure setup still works\n- Always build and exercise your changes before considering the task complete\n\n### Complete Validation Scenario (for major changes):\n```bash\n# 1. Clean build\ntask build\n\n# 2. Setup database (if config.json doesn't exist)\n./bin/semaphore setup  # Choose option 4 (SQLite), use admin/changeme\n\n# 3. Start server\n./bin/semaphore server --config ./config.json\n\n# 4. Test in another terminal\ncurl http://localhost:3000/api/ping  # Should return \"pong\"\ncurl -I http://localhost:3000/       # Should return HTTP 200\n\n# 5. Test web interface manually in browser at http://localhost:3000\n# 6. Test login with admin/changeme if auth-related changes\n```\n\n### Linting and Code Quality\n\n- Frontend linting: `cd web && npm run lint` (has known warnings about console statements and asset sizes - ignore existing issues)\n- Backend linting: `golangci-lint run --timeout=3m` (has known type errors due to module import issues - ignore existing issues) \n- **DO NOT** try to fix existing linting issues unless specifically asked to\n- Always run linting on new code you add to ensure it follows project standards\n- Install golangci-lint if needed: `go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2`\n\n## Common Tasks\n\n### Repository Structure\n```\n.\n├── README.md           - Project documentation  \n├── CONTRIBUTING.md     - Development guidelines\n├── Taskfile.yml       - Task runner configuration\n├── go.mod             - Go module dependencies\n├── web/               - Vue.js frontend application\n│   ├── package.json   - Frontend dependencies\n│   ├── src/           - Vue.js source code\n│   └── public/        - Static assets\n├── cli/               - Go CLI application entry point  \n├── api/               - Go API server endpoints\n├── db/                - Database models and interfaces\n├── services/          - Business logic services\n├── util/              - Utility functions and configuration\n├── bin/               - Built binaries (after build)\n└── config.json       - Runtime configuration (after setup)\n```\n\n### Key Commands Reference\n```bash\n# Install task runner\ngo install github.com/go-task/task/v3/cmd/task@latest\n\n# Install all dependencies (backend + frontend + tools)\ntask deps\n\n# Build application (frontend + backend)\ntask build\n\n# Run tests\ntask test  \n\n# Run linting\ntask lint\n\n# Setup application (interactive)\n./bin/semaphore setup\n\n# Start server\n./bin/semaphore server --config ./config.json\n\n# View available task commands\ntask --list\n```\n\n### Database Options for Development\n\nDuring setup, choose option 4 (SQLite) for the simplest development setup:\n\n- No external database server required\n- Database file stored at the path configured in `config.json` (default under `/tmp/`)\n- Perfect for development and testing\n\n> **Note:** BoltDB (option 2) was removed in Semaphore 2.19. If you have an existing\n> `database.boltdb` file, migrate to SQLite, MySQL, or PostgreSQL before upgrading.\n\n### Frontend Development\n- Vue.js 2.x application in `web/` directory\n- Built with Vue CLI and Vuetify components\n- Build output goes to `api/public/` for serving by Go backend\n- Development server not typically used - Go server serves built assets\n\n### Backend Development  \n- Go application with CLI and API server\n- Uses Gorilla Mux for routing\n- Supports multiple databases: MySQL, PostgreSQL, SQLite (BoltDB was removed in 2.19)\n- Configuration via JSON file or environment variables\n\n## Troubleshooting\n\n### Build Issues\n- If `task` command not found: Install with `go install github.com/go-task/task/v3/cmd/task@latest`\n- If Go version errors: Ensure Go 1.21+ is installed\n- If npm install fails: Ensure Node.js 16+ is installed\n- If build takes too long: This is normal - frontend build can take 60+ seconds\n\n### Runtime Issues  \n- If server won't start: Check config.json exists and database is accessible\n- If web UI shows errors: Check that frontend build completed successfully in `api/public/`\n- If API returns errors: Check server logs for specific error messages\n\n### Database Issues\n\n- For development, use SQLite (option 4) during setup\n- The SQLite database file is created automatically at the configured path\n- If database errors occur, remove the SQLite file and run setup again\n- Configs with `\"dialect\": \"bolt\"` will fail at startup — switch to `sqlite` and re-run setup\n\n## Important Notes\n\n- **NEVER CANCEL** long-running builds or dependency installations\n- Set appropriate timeouts: deps (5+ min), build (3+ min), tests (2+ min)  \n- The application serves the frontend from the Go backend - no separate frontend server needed\n- Configuration is stored in `config.json` after running setup\n- Default admin credentials after setup: admin / changeme\n- Linting has known issues - focus on not introducing new ones\n- Always test changes by running the full application, not just unit tests","AGENTS.md":"Read and follow .claude/CLAUDE.md before starting any work.\n","AGENTS/agents/setup/db-query/README.md":"# db-query\n\nAd-hoc SQL runners for the project's local databases — **one script per database**. Each runs the\nquery as that database's app user, read-only by default, and logs every query it runs.\n\n| Script | Engine | Mode | Database | `.env` file | `.env` keys | Access |\n| --- | --- | --- | --- | --- | --- | --- |\n| `query_<db>.sh` | PostgreSQL / MariaDB | docker / host | `<database>` | `<path>` | `DB_*` | read-only |\n\n## Setup\n\nNone. Each script reads its connection from the project's own `.env`; the path to it is\n`ENV_FILE` in the script's settings block. Credentials are never copied into this folder.\n\n## Usage\n\n~~~shell\nAGENTS/tools/db-query/query_<db>.sh \"SELECT id, email FROM users LIMIT 5\"\nAGENTS/tools/db-query/query_<db>.sh -f report.sql\necho \"SELECT 1\" | AGENTS/tools/db-query/query_<db>.sh\nAGENTS/tools/db-query/query_<db>.sh \"SELECT ...\" -t -A     # trailing flags go to psql / mariadb\n~~~\n\n## Log\n\nEvery query is appended to `AGENTS/tools/db-query/log/queries.log` before it runs — timestamp,\nscript, target, database, user, access mode, the SQL — and its exit code after. Result rows are not\nlogged. The folder is gitignored.\n\n## Traps\n\n- ⚠️ Local only: a remote docker daemon or a non-loopback host is refused. `ALLOW_REMOTE=1` in a\n  script is set only on the user's explicit request — record who asked and when here.\n- ⚠️ Read-only by default: the session refuses any write (`READ_ONLY=1`). It guards against\n  accidents, not against intent — SQL can switch it off. `READ_ONLY=0` in a script is set only on\n  the user's explicit request — record who asked and when here.\n","AGENTS/docs/INDEX.md":"# docs — portable standard by zone\n\nApproaches and conventions that would serve another project, not this project's own facts. One\nfolder per zone, each with its own `INDEX.md`. This file routes to a zone; the zone's index routes\nto the doc.\n\n## Zone index (example)\n\nThe rows below are example data, illustrating the shape a real routing table takes; replace them\nonce an actual zone exists.\n\n| When you need it | Read |\n| --- | --- |\n| Example: Writing backend code: models, routes, permissions, entity codes | `backend/INDEX.md` |\n| Example: Running or writing backend tests — mandatory before the first command | `backend/INDEX.md` |\n| Example: Editing the frontend: app layout, import boundaries, i18n | `frontend/INDEX.md` |\n| Example: Setting up a local stack: reverse proxy, HMR behind a proxy, tunnels — the approach, not this project's ports | `infra/INDEX.md` |\n| Example: Editing an env file or another human-read file: grouping, order, language, line length | `standards/INDEX.md` |\n","AGENTS/tools/wf-stand/README.md":"# wf-stand — run and verify workflows on the local stand\n\nDrives the agent's throwaway Semaphore stand (`http://localhost:3100`, admin/admin123, project 1,\nPro build from HEAD) to execute a workflow end to end and follow every node to a terminal status.\nExists because a workflow run is asynchronous: tasks, approvals and delays resolve over time and\nonly the run-details endpoint shows all of them together.\n\nCall forms (from the repository root):\n\n```\nAGENTS/tools/wf-stand/stand.sh {start|stop|restart|build|status|log [n]}\nAGENTS/tools/wf-stand/seed.sh\nAGENTS/tools/wf-stand/seed-outputs.sh          # PROJECT_NAME=\"…\" to seed another project\nAGENTS/tools/wf-stand/run.sh <workflow_id> [--approve|--reject] [--stop-after SEC] [--expect success|failed|stopped] [--timeout SEC] [--run ID]\nAGENTS/tools/wf-stand/runner.sh {start|stop|status|tag|untag}   # PID=<project> TAG=<tag>; remote runner for the stand\nAGENTS/tools/wf-stand/inputs.sh <workflow_id> [run_id]           # outputs stored per producer + \"INPUT name=value\" lines per consumer\nNODE_PATH=~/.npm/_npx/9833c18b2d85bc59/node_modules node AGENTS/tools/wf-stand/shoot-run.cjs <workflow_id> [run_id] [out.png] [--dark]\nAGENTS/tools/wf-stand/seed-docs.sh                                # project \"Demo Stand\" + workflow \"Build and deploy\" for the docs screenshots\nAGENTS/tools/wf-stand/seed-survey.sh                              # workflow \"Survey: workflow variables\" (survey + node bindings) in the seed-outputs project\nNODE_PATH=… node AGENTS/tools/wf-stand/shoot-editor.cjs <workflow_id> [node_title] [out.png] [--dark]   # editor with a node selected: survey panel, canvas, bindings\nNODE_PATH=… node AGENTS/tools/wf-stand/shoot-run-dialog.cjs <workflow_id> [out_dir] [--dark]       # starts a run through the survey dialog; shoots the dialog and the run view's Variables strip\nPID=<project> WF=<workflow> NODE_PATH=… node AGENTS/tools/wf-stand/shoot-docs.cjs   # the four screenshots of workflows.md § Outputs and inputs\n```\n\nExamples:\n\n```\n$ AGENTS/tools/wf-stand/stand.sh build && AGENTS/tools/wf-stand/stand.sh start\nstand: up at http://localhost:3100 (pid 40366)\n$ AGENTS/tools/wf-stand/seed.sh\nworkflows happy=5 branches=6                      # ids also in /tmp/semaphore-stand/wf-exec-ids.txt\n$ AGENTS/tools/wf-stand/run.sh 5 --approve --expect success\nrun 4 started (workflow 5) — UI: http://localhost:3100/project/1/workflows/5/runs/4\n17:30:14 approval version=0.1.0 n18/task=task#6:success n19/approval=approval:pending n20/delay=- n21/task=-\n17:30:14 node 19 -> approved\n17:30:24 success version=0.1.0 n18/task=task#6:success n19/approval=approval:approved n20/delay=delay:success n21/task=task#7:success\nfinal: success (run 4)\n$ AGENTS/tools/wf-stand/run.sh 6 --stop-after 8 --expect stopped\nfinal: stopped (run 6)\n```\n\nSeeded workflows: \"Exec happy\" (task → approval → delay 5 s → task, ends `success` with `--approve`)\nand \"Exec branches\" (ok → fail; on_failure → recover, on_success → never started; always → 30 s\nslow task; ends `failed`). Templates are `bash` scripts from the local repo\n`/tmp/semaphore-stand/wf-repo` (`ok.sh`, `fail.sh`, `slow.sh`), so no network or Ansible is needed.\n\n`seed-outputs.sh` seeds the project \"Artifacts QA\" (project 1 since the 2026-10-07 rebuild) for\ntesting workflow outputs by hand: 16 templates (bash producers for every rule of the contract —\nvalid, empty, bad JSON, bad name, 101 outputs, 40 KB value, outputs-then-exit-1, symlink; bash and\nAnsible consumers with one survey var of every type; Terraform / Tofu with a `sensitive` and a\nhyphenated output; Ansible `set_stats`) and four workflows: 1 \"producers -> consumers\" (expect\n`success`, see `task.artifacts` of tasks of nodes 2–4), 2 \"invalid producers\" (expect `failed`,\neach bad producer `error` with the reason in its log, \"empty\" `success` without artifacts),\n3 \"failed producer\" (expect `failed`, no artifacts stored, `on_failure` consumer ran), 4 \"tofu ->\nconsume\" (expect `success`). Scripts: `/tmp/semaphore-stand/wf-repo/produce*.sh`, `consume.sh`,\n`tf/main.tf`, `playbooks/*.yml`. Ids land in `/tmp/semaphore-stand/wf-outputs-ids.txt`.\n\nSince 2026-10-08 the same seed adds the mapping checks: templates \"bash: produce renamed outputs\" and\n\"ansible: set_stats renamed outputs\" (output names `tag`, `count`, `flag`, `subnets`, `cfg`, `envname` that\nmatch no survey variable, so only an explicit edge mapping can deliver them) and workflows 9 \"Inputs: explicit\nmapping\" (renamed producers → bash and ansible consumers over `explicit` edges, `image_tag←tag`,\n`replicas←count`, …), 10 \"Inputs: by_name through approval\" (produce → approval → consumers, run with\n`--approve`), 11 \"Inputs: fan-in by_name vs explicit\" (produce by name and renamed explicitly into one consumer\nwith convergence all: expect `image_tag` from the explicit edge, `replicas` by name), 12 \"Inputs: explicit empty\nlist\" (the edge passes nothing). Consumers print one `INPUT name=value` line per survey variable (bash: parsed\n`k=v` args plus `env:env_name` — a `target: env` variable keeps its own name, it is not upper-cased; ansible: a debug list), so `inputs.sh <wf>` shows producer outputs next to what\nthe consumer received. Expected since the resolver landed (2026-10-08): workflow 1 and 10 — the consumer's\n`INPUT` lines repeat the producer's `image_tag`, `replicas`, `enabled`, `subnet_ids`, `config` (by name;\n`env-name` with a hyphen is not delivered); workflow 9 — the renamed values (`image_tag=renamed-…`,\n`replicas=7`, `env:env_name=prod`); workflow 11 — `image_tag=renamed-…` from the explicit edge and `replicas=3`\nby name; workflow 12 — only `image_tag=static-from-node`, `replicas=1` (the default) and `env:env_name=static-env`;\nworkflow 3 — the on_failure consumer is `error` before start with \"Required input(s) without a value: image_tag\"\nin its log. The task log of every consumer starts with the engine's `Input \"<var>\" <- output \"<key>\" of node N\n(task #M)` lines, so a value's source is always one line above the value.\n\nSince 2026-10-09 the seed also covers the Jinja rule (Ansible never evaluates an expression inside an\nextra var — every value reaches it through a file, the ones with a Jinja2 delimiter tagged `!unsafe`): template \"bash: produce jinja outputs\"\n(`produce_jinja.sh` writes `image_tag`, `config` and `replicas` holding `{{ lookup('pipe', …) }}`,\n`{{ lookup('env', 'HOME') }}`, `{{ 6*7 }}`), the variable group \"vars group with jinja\" (`group_expr: \"{{ 2*21 }}\"`),\ntemplate \"ansible: consume + vars group\" and workflows 13 \"Inputs: jinja injection\" (→ ansible and bash\nconsumers) and 14 \"Inputs: jinja vs vars group\". Expected on the server and on the runner: `INPUT image_tag={{\nlookup('pipe', 'echo INJECTED-$(id -un)') }}`, `config` with the expressions verbatim, `replicas=1` (the\ndefault — `{{ 6*7 }}` is not an int) and `INPUT group_expr={{ 2*21 }}` — the variable group is data too. A\nconsumer log containing `INJECTED-` or `group_expr=42` means Ansible evaluated a value: a regression.\n`inputs.sh` right after `run.sh` on the runner can miss the last log lines; re-run it with the run id.\nTrap: the template API ignores `environment_id`, pass `environment_ids`.\n\nSince 2026-10-10 the stand also checks the secret extra vars (TASK@CEA87DDD08): the survey secret\n`token` and the variable-group secret `db_password` (type `var`, added to \"vars group with jinja\" by hand\nthrough `PUT /project/1/environment/1` with `secrets:[{type:\"var\",name,secret,operation:\"create\"}]`) reach\nAnsible through a second, vault-encrypted `--extra-vars @task_<id>_secret_vars_*.yml` with a one-off\n`--vault-id <guid>@prompt`. `playbooks/consume.yml` prints `INPUT token=…` and `INPUT db_password=…`;\ntemplate 37 \"ansible: consume + own vault\" (access key \"tpl vault\", type login_password, vault `default`)\nruns `playbooks/consume_vault.yml`, which loads `playbooks/vaulted.yml` (encrypted by `ansible-vault` with\nthat key's password) through `vars_files` and prints `INPUT vaulted_var=…`. Expected on the server and on\nthe runner: `INPUT token=s3cr3t {{ 1+1 }}`, `INPUT db_password=p@ss {{ 7*6 }}` literally; `ps -axo args=`\nshows `--vault-id=<guid>@prompt` and two `--extra-vars @`, never a secret or the password; `grep -rl\ns3cr3t /tmp/semaphore-stand/tmp` (or `runner-tmp`) finds nothing while the task runs; no `task_*_vars_*`\nfile is left after. With `SEMAPHORE_LOG_LEVEL=DEBUG SEMAPHORE_DEBUG_FILTER=extra_vars` exported before\n`stand.sh start` / `runner.sh start`, the server and runner logs show seven `context=extra_vars` lines per\ntask (split, two files written, vault added, two files removed, password forgotten). Checked 2026-10-10:\nserver task 162, runner task 164, own vault task 171, regression runs 39 (wf 13) and 40 (wf 14).\nTraps: a vault-encrypted file must not sit in `playbooks/group_vars/` — Ansible loads it for every playbook\nof the directory and a template without that vault fails with \"Decryption failed\"; `vars_files` of the\nrepository are still templated (`{{ 3*3 }}` prints `9`) — only values Semaphore passes are `!unsafe`;\n`runner.sh start | tail` hangs because the runner inherits the pipe — redirect to a file instead; a PUT of a\ntemplate without the `vaults` field deletes its vaults, which is why `runner.sh tag/untag` re-reads each\ntemplate by id before the PUT.\n\nDocs screenshots (2026-10-11): `seed-docs.sh` seeds a separate project \"Demo Stand\" (the name the published\neditor screenshot already shows) with \"Provision network\" (bash, outputs `vpc_id`, `subnet_ids`), \"Build image\"\n(bash, outputs `image_tag`, `replicas` — the values quoted in the user guide), \"Deploy to cluster\" (ansible\n`playbooks/deploy.yml`, survey `image_tag`, `replicas`, `subnets`) and the workflow \"Build and deploy\":\nProvision → Build → Deploy by name plus Provision → Deploy explicit (`subnets ← subnet_ids`). Run it once\n(`PID=<p> run.sh <wf> --expect success`), then `shoot-docs.cjs` writes `workflow-inputs-explicit.png`,\n`workflow-inputs-by-name.png`, `workflow-run-outputs.png` and `task-outputs.png` (dialog clipped) to\n`/tmp/semaphore-stand/shots-docs`; convert with `cwebp -q 88` into `docs/static/assets/`. Traps: a workflow must\nhave exactly one root node (the explicit edge alone would make Provision and Build two roots); key hints need a\nrun of the current revision, so re-run after any re-seed; the editor fits a three-node graph at ~45 % on a 1440 px\nviewport — the script collapses the palette (`.WorkflowEditor__sideToggle`) and clicks the fit control after the\nside panel opens instead of zooming in, which pushes nodes under the panel.\n\nSeeding traps learned there: a Terraform-family template must be POSTed without `inventory_id`\n(the API creates a `terraform-workspace` inventory; a shared one gets claimed and hidden) and with\n`task_params.auto_approve`, or a plan with changes parks the task in `waiting_confirmation`; a\nworkflow must have exactly one root node; `survey_vars[].default_value` is a plain string; a JSON\n`\\\"` inside `$( )` inside a double-quoted bash string is mangled — build such fragments in a\nvariable first.\n\nTraps: `run.sh` exits 1 when `--expect` does not match or `--timeout` (default 180 s) passes;\n`--approve` resolves every pending approval it sees, so use `--run ID` to only follow an existing\nrun. `stand.sh build` rebuilds the Vue app into `api/public` (a tracked-ignored embed input) and\nthe binary at `/tmp/semaphore-stand-bin`; it needs the `pro_impl` workspace for Workflows to be\nenabled. The stand DB is `/tmp/semaphore-stand/database-wf.sqlite` and persists between sessions.\n\n`runner.sh` registers a global runner \"wf-runner\" (tag `wf`, local executor, `/tmp/semaphore-stand/runner-config.json`,\nlog `runner.log`) and starts it from the same binary; `tag` sets `runner_tag` on every template of project\n`PID` (default 1) so their tasks go to it, `untag` sends them back to the server. The remote-runner check\nof workflow outputs (2026-10-08): `runner.sh start && runner.sh tag && run.sh 1 --expect success && run.sh 2\n--expect failed`, then `sqlite3 /tmp/semaphore-stand/database-wf.sqlite \"select id,status,runner_id,length(artifacts)\nfrom task order by id desc limit 13\"` shows `runner_id` of the runner and `artifacts` on the producers only.\nTraps: the token is returned once at creation, so a runner whose config file is gone is deleted and recreated;\nstop the runner and `untag` before a session that expects tasks to run on the server, or they wait forever;\n`status` keeps saying `online` for up to 120 s after `stop` (the server's heartbeat timeout), trust the\n`process:` line.\n\n`seed-survey.sh` (2026-10-16, TASK@B197C72B7F) needs the templates of `seed-outputs.sh` and adds workflow\n\"Survey: workflow variables\": survey `env` (enum, default `staging`), `image_tag` (required), `replicas` (int,\ndefault 2); node 2 \"bash: consume\" bound `image_tag←image_tag`, `replicas←replicas`, `env_name←env` on top of\nstatic `image_tag=static-from-node`; node 3 the same template without bindings (must receive nothing although\nthe names match); node 4 \"bash: produce renamed outputs\" → node 5 \"bash: consume\" over an explicit edge\n`replicas←count` plus the binding `image_tag←image_tag`. Start a run with a body:\n`POST …/workflows/<id>/runs {\"environment\":{\"image_tag\":\"1.2.3\",\"env\":\"prod\"}}` (ids in\n`/tmp/semaphore-stand/wf-survey-ids.txt`). `shoot-editor.cjs` logs in, opens the editor, clicks the first card\nwhose title matches, scrolls the first bound survey field into view and writes the page, a crop of the task\nparams card (`<out>-fields.png`) and the open assign menu (`<out>-assign.png`). `shoot-run-dialog.cjs` opens the\nworkflow page, presses Run, checks that an empty required answer is refused, fills `image_tag` (`IMAGE_TAG`,\ndefault 1.2.3) and the first enum option, starts the run and shoots `run-dialog-invalid.png`, `run-dialog.png` and\n`run-view-variables.png` in `out_dir`; it prints the run id and the Variables chips.\n\n⚠️ 2026-10-16: macOS purges files (not directories) under `/tmp` that were not touched for ~3 days —\n`config-wf.json`, `database-wf.sqlite`, the admin user and every script in `wf-repo` vanished between sessions.\n`stand.sh build` + a fresh `config-wf.json` (sqlite at `$DIR/database-wf.sqlite`, port `:3100`, three\n`openssl rand -base64 32` keys, `use_remote_runner: true`), `migrate`, `user add --admin --login admin\n--password admin123` and the seeds bring it back; `wf-repo` scripts must be recreated before any run.\n","AGENTS/work/workflow-editor/mocks/README.md":"# mocks — approved mock, screenshots and the scripts that shoot them\n\nDesign artefacts of the editor redesign (`TASK@88179c9bc1`); decisions are in\n`../editor-architecture.md`, not here. Every script targets the local stand\n`http://localhost:3100` (admin/admin123, project 1) and Playwright loaded through `NODE_PATH`\nwith `channel: 'chrome'` — stand and Playwright recipes are in `AGENTS/memory/ui-stands.md`.\n\n## Files\n\n| File | What it is |\n| --- | --- |\n| `2026-09-26-workflow-node-mock.html` | Approved static mock: editor canvas, run view, card states, empty state; `#dark` in the URL flips the theme. Open from the repo checkout — MDI icons load from `web/node_modules`. Its \"Tab opens quick-add\" hint is stale (see ⚠️ in `editor-architecture.md`). |\n| `2026-09-26-workflow-node-mock-{light,dark}.png` | Renders of the mock as approved. |\n| `workflow-editor/editor-{light,dark}.png`, `editor-selected-*.png`, `editor-quickadd-light.png`, `editor-legacy-*.png`, `editor-empty-light.png`, `run-{light,dark}.png` | Screenshots of the shipped UI at 1440 × 900 @2x, shot by `shoot-wf.cjs`. \"Legacy\" is a workflow saved without positions (auto layout). |\n| `workflow-editor/editor-interactions.png` | End state of the `interact-wf.cjs` checklist. |\n| `workflow-editor/run-approval-pending.png`, `run-delay-countdown.png` | Run view with a pending approval and the delay countdown, from `approve-wf.cjs`. |\n| `workflow-editor/editor-edge-explicit-{light,dark}.png`, `editor-edge-byname-{light,dark}.png`, `run-outputs-{light,dark}.png`, `task-outputs-{light,dark}.png` | Workflow inputs (2026-10-09, `TASK@1845d9d4d7` stage 7): the edge panel with \"Map inputs explicitly\" and the mapping table (keys suggested from the last run), the by_name panel with the destination's survey variables (a check on those the source produced), the run view with \"N outputs\" on producer cards and the swap icon on explicit pills, the task dialog's Outputs panel. Shot by `shoot-inputs.cjs` on the \"Artifacts QA\" project. |\n| `survey/editor-bindings.png`, `survey/editor-bindings-fields.png`, `survey/editor-bindings-assign.png`, `survey/editor-problem.png` | Workflow survey variables (2026-10-16, `TASK@B197C72B7F` stage 7, redesigned the same day on the owner's request): the editor with the survey in the left panel, \"3 from workflow\" on a bound card and the node panel whose bound survey fields are read-only with a link icon and an unbind cross; a crop of the task params card; the \"assign a workflow variable\" menu open next to an unbound field; the same workflow after the variable \"Image tag\" was deleted — \"2 problems\", Save disabled, badges on the bound nodes and the error under the orphaned field. Shot by `AGENTS/tools/wf-stand/shoot-editor.cjs` on workflow 11 of \"Artifacts QA\" (`seed-survey.sh`). |\n| `survey/run-dialog.png`, `survey/run-dialog-invalid.png`, `survey/run-view-variables.png` | Workflow survey at run time (2026-10-16, `TASK@B197C72B7F` stage 8): the \"Run\" dialog with the survey fields seeded from the defaults, the same dialog refusing an empty required answer, and the run view with the \"Variables\" strip (`run.environment`) under the toolbar. Shot by `AGENTS/tools/wf-stand/shoot-run-dialog.cjs` on workflow 11 of \"Artifacts QA\". |\n| `workflow-editor/editor-revision-active-run.png`, `run-after-edit.png` | Revisions (2026-09-27): the editor with the \"rev. 3\" and \"1 active run\" chips, and run #8 still rendered on its revision 1 after two later saves. Shot with `AGENTS/tools/wf-stand/shoot-run.cjs`. |\n\n## Scripts\n\n| Script | Does | Needs |\n| --- | --- | --- |\n| `seed-wf.sh` | Seeds five templates (one with an XSS name), the \"Release\" workflow with positions, the position-less \"Nightly checks\" (with a `<script>` note), starts one Release run; writes ids to `/tmp/semaphore-stand/wf-ids.txt`. Idempotent for templates. | Running stand, repository 1 and inventory 1 in project 1. |\n| `shoot-wf.cjs` | Editor/run/legacy/empty screenshots in light and dark; output `OUT` (default `/tmp/semaphore-stand/shots-wf`), `RUN` selects the run id. | `wf-ids.txt`. |\n| `interact-wf.cjs` | 16 PASS/FAIL checks on the editor: quick-add from the port, palette click, drag connect, cycle guard, pill condition change, snap, Delete, undo/redo round trip, problem badge + disabled Save, leave guard. Exits 1 on any FAIL or page error. | `wf-ids.txt`. Mutates the Release workflow in the browser but does not save. |\n| `approve-wf.cjs` | Approve inside the card, delay countdown ticks, viewport survives polling, task log opens on click. | `/tmp/semaphore-stand/wf3.txt` with `<workflow id> <run id>` of a run waiting at an approval — seeded by hand, not by `seed-wf.sh`. |\n| `keys-wf.cjs` | Keyboard pan/zoom/fit/reset in the editor and the run view; asserts no effect while typing in the properties panel. | `wf-ids.txt`, run 2 of Release. |\n| `shoot-inputs.cjs` | Shoots the four inputs screenshots above and runs 7 PASS/FAIL checks (checkbox state, mapping rows, hint line, by_name chips with a matched output, output count on a card, Outputs panel in the task dialog). `--dark` for the dark theme; `WF` (default 9, explicit workflow), `WF_BYNAME` (default 1), `OUT` (default `/tmp/semaphore-stand/shots-inputs`). Edges are selected by a dispatched `mousedown` on the path — a real click lands on the condition pill. | `AGENTS/tools/wf-stand/seed-outputs.sh` seeded, a finished run of the current revision of both workflows (`run.sh 9`, `run.sh 1`) for the key hints. |\n| `interact-inputs.cjs` | 10 PASS/FAIL checks of the inputs round trip on workflow `WF` (default 12): select the explicit edge, not dirty before editing, type a key, dirty, explicit icon on the pill, Save → API shows the pair, uncheck → by_name chips, Save → fields gone, original graph restored by PUT. Exits 1 on any FAIL or page error. | Running stand with the seeded project; mutates workflow 12 and restores it. |\n| `shoot-nodes.cjs` | Crops one card per node kind (task hovered with the \"+\", approval, delay, note) with edge pills hidden and zoom 1; source of `docs/static/assets/workflow-node-*.webp`. | `WF` env = id of the \"Docs node kinds\" workflow on the stand (default 4). |\n| `make-hotkeys-svg.py <out.svg>` | Generates the keyboard cheat-sheet `docs/static/assets/workflow-hotkeys.svg`; edit `COLUMNS` and re-run when a shortcut changes. | Python 3 only. |\n","CLAUDE.md":"AGENTS/agent-primary.md"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"Read and follow .claude/CLAUDE.md before starting any work.\n","isInternal":false,"tokens":16,"sizeBytes":60},{"name":"CLAUDE.md","path":".claude/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.claude/CLAUDE.md","title":".claude - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Claude Code Instructions\n\n# Documentation\n\nThe documentation stored in /docs directory. It is a submodule with official Semaphore UI documentation\nin on Docusaurus engine.\n\nYou should actualize the documentation for new and updated featues.\n\n### Generated reference pages\n\n`docs/docs/reference/configuration.md` and `docs/docs/reference/cli/commands.md` are generated\nfrom this repository and must never be edited by hand:\n\n- `tools/docsref` reads the struct tags and doc comments of `util.ConfigType`.\n- `tools/clidocs` walks the real Cobra command tree from `cli/cmd`.\n\nAfter changing a configuration option or a CLI command, run `task docs:gen` and commit\nthe result together with the code. The `docs-reference` job in `.github/workflows/dev.yml`\nfails when the committed pages differ from what the generators produce.\n\nA config field with no doc comment falls back to `tools/docsref/descriptions.json`.\nPrefer writing the doc comment: `docsref` prints how many options still depend on that\nfile, and the number should only fall.\n\n## Writing Plans\n\nAll plans, tasks, researches for AI agents stored in folder AGENTS.\n\nEach plan has markdown-format and stored in folder AGENTS/plans/<version>.\n\nPlan can be split to tasks. Each task describes in details how to implement some part of some plan.\n\n## Code Style\n\n1. Do not use global variables. Global variables are forbidden.\n\n## High Availability Support\n\nAll solutions must work in High Availability mode.\n\n## Security Is the #1 Priority\n\nAll solutions must be secure by design. Do not consider any solution that introduces security risks.\n\n## How to do research\n\nUse MCP server `research` if your asks you to research something.\n\n## Writing Tests\n\nUse `github.com/stretchr/testify/assert` (and `require` when a failure should stop the test immediately).\n\n### Run tests\n\n```bash\ngo test ./path/to/package/ -run TestFunctionName -v -count=1\n```\n\n### Rules\n\n- Test file goes next to the source: `foo.go` → `foo_test.go`, same package.\n- Use `assert.Equal`, `assert.Empty`, `assert.Nil`, `assert.True`, `assert.Panics`, etc. Never use raw `if`/`t.Fatalf` for assertions.\n- Use `require` (instead of `assert`) when subsequent lines depend on the check passing (e.g., `require.NoError` before using a result).\n- Use table-driven tests with `t.Run` for multiple inputs to the same function.\n- Use `t.TempDir()` for temporary files — cleanup is automatic.\n- When testing code that uses package-level globals (e.g., `util.Config`), initialize them in a helper and reset state between tests.\n- When testing code that reads `os.Stdin`, swap it with `os.Pipe()` and restore in `defer`.\n\n### Template\n\n```go\npackage mypkg\n\nimport (\n    \"testing\"\n\n    \"github.com/stretchr/testify/assert\"\n    \"github.com/stretchr/testify/require\"\n)\n\nfunc TestMyFunction(t *testing.T) {\n    // setup\n    input := \"value\"\n\n    // act\n    result, err := MyFunction(input)\n\n    // assert\n    require.NoError(t, err)\n    assert.Equal(t, \"expected\", result)\n}\n\nfunc TestMyFunction_TableDriven(t *testing.T) {\n    tests := []struct {\n        name     string\n        input    string\n        expected string\n    }{\n        {\"short input\", \"abc\", \"ABC\"},\n        {\"empty input\", \"\", \"\"},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            assert.Equal(t, tt.expected, MyFunction(tt.input))\n        })\n    }\n}\n\nfunc TestMyFunction_Panics(t *testing.T) {\n    assert.Panics(t, func() {\n        MyFunction(nil)\n    })\n}\n```\n\n### Common assertions\n\n| Assertion | Use when |\n|-----------|----------|\n| `assert.Equal(t, expected, actual)` | Comparing values |\n| `assert.NotEqual(t, a, b)` | Values must differ |\n| `assert.Nil(t, val)` / `assert.NotNil(t, val)` | Pointer/interface checks |\n| `assert.Empty(t, val)` | String, slice, or map is zero-length |\n| `assert.Contains(t, str, substr)` | Substring or element in collection |\n| `assert.True(t, cond)` / `assert.False(t, cond)` | Boolean conditions |\n| `assert.NoError(t, err)` / `assert.Error(t, err)` | Error checks |\n| `assert.ErrorContains(t, err, \"msg\")` | Error with specific message |\n| `assert.Panics(t, func(){...})` | Code must panic |\n| `require.NoError(t, err)` | Stop test immediately on error |\n\n### Initializing `util.Config` in tests\n\nMany packages depend on `util.Config` which is a `*ConfigType` pointer (nil by default). Initialize before use:\n\n```go\nfunc setupConfig() {\n    if util.Config == nil {\n        util.Config = &util.ConfigType{}\n    }\n    // Initialize nested pointers as needed:\n    if util.Config.Runner == nil {\n        util.Config.Runner = &util.RunnerConfig{}\n    }\n}\n```\n\n### HTTP handler tests\n\nUse `net/http/httptest`:\n\n```go\nfunc TestMyHandler(t *testing.T) {\n    req := httptest.NewRequest(http.MethodGet, \"/api/endpoint\", nil)\n    w := httptest.NewRecorder()\n\n    myHandler(w, req)\n\n    assert.Equal(t, http.StatusOK, w.Code)\n    assert.Contains(t, w.Body.String(), \"expected\")\n}\n```\n","isInternal":false,"tokens":1184,"sizeBytes":4935},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"AGENTS/agent-primary.md","isInternal":false,"tokens":6,"sizeBytes":23},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# Semaphore UI Development Instructions\n\n**Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**\n\nSemaphore UI is a modern web interface for managing popular DevOps tools like Ansible, Terraform, PowerShell, and Bash scripts. It's built with Go (backend) and Vue.js (frontend), using Task runner for build automation.\n\n## Working Effectively\n\n### Bootstrap, build, and test the repository:\n- Install Go 1.21+ (currently requires go version 1.21 or higher)\n- Install Node.js 16+ \n- Install Task runner: `go install github.com/go-task/task/v3/cmd/task@latest`\n- Install dependencies: `task deps` -- takes 3 minutes first time (faster with cache). NEVER CANCEL. Set timeout to 5+ minutes.\n- Build the application: `task build` -- takes 1.5 minutes. NEVER CANCEL. Set timeout to 3+ minutes.\n- Run tests: `task test` -- takes 33 seconds. NEVER CANCEL. Set timeout to 2+ minutes.\n\n### Run the application:\n- ALWAYS run the bootstrapping steps first\n- Setup database and admin user: `./bin/semaphore setup` (interactive, use SQLite option 4 for development)\n- Start server: `./bin/semaphore server --config ./config.json`\n- Web UI: http://localhost:3000 (login: admin / changeme)\n- API: http://localhost:3000/api/ (test with: `curl http://localhost:3000/api/ping`)\n\n## Validation\n\n- **CRITICAL**: Always manually validate any new code by building and running the application.\n- ALWAYS run through at least one complete end-to-end scenario after making changes:\n  1. Build the application: `task build`\n  2. Start the server: `./bin/semaphore server --config ./config.json`\n  3. Test API endpoint: `curl http://localhost:3000/api/ping` (should return \"pong\")\n  4. Access web UI at http://localhost:3000 and verify it loads\n  5. For auth changes: Test login with admin/changeme\n- For significant changes, run full setup process to ensure setup still works\n- Always build and exercise your changes before considering the task complete\n\n### Complete Validation Scenario (for major changes):\n```bash\n# 1. Clean build\ntask build\n\n# 2. Setup database (if config.json doesn't exist)\n./bin/semaphore setup  # Choose option 4 (SQLite), use admin/changeme\n\n# 3. Start server\n./bin/semaphore server --config ./config.json\n\n# 4. Test in another terminal\ncurl http://localhost:3000/api/ping  # Should return \"pong\"\ncurl -I http://localhost:3000/       # Should return HTTP 200\n\n# 5. Test web interface manually in browser at http://localhost:3000\n# 6. Test login with admin/changeme if auth-related changes\n```\n\n### Linting and Code Quality\n\n- Frontend linting: `cd web && npm run lint` (has known warnings about console statements and asset sizes - ignore existing issues)\n- Backend linting: `golangci-lint run --timeout=3m` (has known type errors due to module import issues - ignore existing issues) \n- **DO NOT** try to fix existing linting issues unless specifically asked to\n- Always run linting on new code you add to ensure it follows project standards\n- Install golangci-lint if needed: `go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2`\n\n## Common Tasks\n\n### Repository Structure\n```\n.\n├── README.md           - Project documentation  \n├── CONTRIBUTING.md     - Development guidelines\n├── Taskfile.yml       - Task runner configuration\n├── go.mod             - Go module dependencies\n├── web/               - Vue.js frontend application\n│   ├── package.json   - Frontend dependencies\n│   ├── src/           - Vue.js source code\n│   └── public/        - Static assets\n├── cli/               - Go CLI application entry point  \n├── api/               - Go API server endpoints\n├── db/                - Database models and interfaces\n├── services/          - Business logic services\n├── util/              - Utility functions and configuration\n├── bin/               - Built binaries (after build)\n└── config.json       - Runtime configuration (after setup)\n```\n\n### Key Commands Reference\n```bash\n# Install task runner\ngo install github.com/go-task/task/v3/cmd/task@latest\n\n# Install all dependencies (backend + frontend + tools)\ntask deps\n\n# Build application (frontend + backend)\ntask build\n\n# Run tests\ntask test  \n\n# Run linting\ntask lint\n\n# Setup application (interactive)\n./bin/semaphore setup\n\n# Start server\n./bin/semaphore server --config ./config.json\n\n# View available task commands\ntask --list\n```\n\n### Database Options for Development\n\nDuring setup, choose option 4 (SQLite) for the simplest development setup:\n\n- No external database server required\n- Database file stored at the path configured in `config.json` (default under `/tmp/`)\n- Perfect for development and testing\n\n> **Note:** BoltDB (option 2) was removed in Semaphore 2.19. If you have an existing\n> `database.boltdb` file, migrate to SQLite, MySQL, or PostgreSQL before upgrading.\n\n### Frontend Development\n- Vue.js 2.x application in `web/` directory\n- Built with Vue CLI and Vuetify components\n- Build output goes to `api/public/` for serving by Go backend\n- Development server not typically used - Go server serves built assets\n\n### Backend Development  \n- Go application with CLI and API server\n- Uses Gorilla Mux for routing\n- Supports multiple databases: MySQL, PostgreSQL, SQLite (BoltDB was removed in 2.19)\n- Configuration via JSON file or environment variables\n\n## Troubleshooting\n\n### Build Issues\n- If `task` command not found: Install with `go install github.com/go-task/task/v3/cmd/task@latest`\n- If Go version errors: Ensure Go 1.21+ is installed\n- If npm install fails: Ensure Node.js 16+ is installed\n- If build takes too long: This is normal - frontend build can take 60+ seconds\n\n### Runtime Issues  \n- If server won't start: Check config.json exists and database is accessible\n- If web UI shows errors: Check that frontend build completed successfully in `api/public/`\n- If API returns errors: Check server logs for specific error messages\n\n### Database Issues\n\n- For development, use SQLite (option 4) during setup\n- The SQLite database file is created automatically at the configured path\n- If database errors occur, remove the SQLite file and run setup again\n- Configs with `\"dialect\": \"bolt\"` will fail at startup — switch to `sqlite` and re-run setup\n\n## Important Notes\n\n- **NEVER CANCEL** long-running builds or dependency installations\n- Set appropriate timeouts: deps (5+ min), build (3+ min), tests (2+ min)  \n- The application serves the frontend from the Go backend - no separate frontend server needed\n- Configuration is stored in `config.json` after running setup\n- Default admin credentials after setup: admin / changeme\n- Linting has known issues - focus on not introducing new ones\n- Always test changes by running the full application, not just unit tests","isInternal":false,"tokens":1630,"sizeBytes":6889},{"name":"SKILL.md","path":".claude/skills/semaphore-checkout/SKILL.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.claude/skills/semaphore-checkout/SKILL.md","title":"semaphore-checkout","category":"anthropic-skill","format":"markdown","content":"---\nname: semaphore-checkout\ndescription: >-\n    Create and switch to a branch with the same name in both repos — the root repo and pro_impl. \n    Usage: /semaphore-checkout <branch_name>\n---\n\nCreate a branch named `<branch_name>` in both repositories at once — the root repo\n(current directory) and the PRO module repo (`./pro_impl`) — and switch both to it.\n\nCI depends on branch names matching: GitHub Actions clones the `pro_impl` branch\nwith the same name as the root repo branch and falls back to `main` if it does not\nexist. This skill keeps the two repos in sync.\n\n## Arguments\n\n- `<branch_name>` (required) — the branch to create. If not provided, ask the user\n  for it. Validate it with `git check-ref-format --branch <branch_name>` before use.\n\n## Behavior\n\nFor each repo — root (`.`) first, then `./pro_impl`:\n\n1. Check the working tree is clean (`git status --porcelain`). If there are\n   uncommitted changes in either repo, stop and report which repo is dirty —\n   do not stash or discard anything.\n2. Fetch the latest state: `git fetch origin`.\n3. Create the branch from the up-to-date default branch and switch to it:\n   - root repo: `git checkout -b <branch_name> origin/develop`\n   - pro_impl: `git checkout -b <branch_name> origin/main`\n4. If the branch already exists locally, just `git checkout <branch_name>`\n   instead of creating it. If it exists only on origin, check it out tracking\n   the remote branch.\n\nDo not push the new branches — they are pushed later with the first commit.\n\n## Rules\n\n- Never force-create (`-B`) or reset an existing branch.\n- Both repos must end up on the same branch name; if step for `pro_impl` fails\n  after the root repo already switched, report the inconsistent state clearly.\n- Report the final state at the end: branch name and start point in each repo.\n\n## Example\n\n```\n/semaphore-checkout fix/task-env-vars\n```\n\nResult:\n\n```\nRoot repo:  fix/task-env-vars (from origin/develop)\npro_impl:   fix/task-env-vars (from origin/main)\n```\n","frontmatter":{"name":"semaphore-checkout","description":"Create and switch to a branch with the same name in both repos — the root repo and pro_impl.  Usage: /semaphore-checkout <branch_name>"},"isInternal":false,"tokens":484,"sizeBytes":2005},{"name":"SKILL.md","path":".claude/skills/semaphore-cli-docs/SKILL.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.claude/skills/semaphore-cli-docs/SKILL.md","title":"semaphore-cli-docs","category":"anthropic-skill","format":"markdown","content":"---\nname: semaphore-cli-docs\ndescription: >\n  Generate or update the CLI documentation under `docs/docs/reference/cli/` from the Cobra command code in\n  `cli/`. Use this skill whenever the user asks to \"update the CLI docs\", \"actualize/sync the CLI documentation\",\n  \"document the CLI\", \"the CLI docs are out of date\", \"regenerate the runner/user/vault/project/migrate docs\", or\n  after adding or changing a Cobra command, subcommand, flag, alias, or required-argument check anywhere under\n  `cli/cmd/`. Covers every command group (`users`, `projects`, `vaults`, `runner`, `migrate`) plus the operational\n  commands (`server`, `setup`, `version`) and the global flags on the root command. Trigger when the user adds a\n  new `*Cmd` Cobra command, a new `PersistentFlags()` call, or renames/removes a CLI flag.\n---\n\n# Semaphore — CLI Documentation Generator\n\nThis skill produces and maintains the CLI reference pages under\n`docs/docs/reference/cli/` and the CLI index `docs/docs/reference/cli/README.md`. Every page is **derived from the\nCobra command code** in `cli/cmd/`. If a command, subcommand, or flag isn't in the code, it must not be in the\ndocs; if a flag's name, default, or required-ness changed, the docs must change with it.\n\n> `docs/` is a **git submodule** (the documentation site). Editing files under `docs/docs/...` and\n> `docs/sidebars.js` is correct; those changes are committed to the submodule, not the main repo.\n\nThe docs are the human-facing contract. The Go code is the source of truth. When they disagree, the code wins —\n**but** read the command's `Run` body, not just its help strings, because help text drifts (see Pitfalls).\n\n> **`docs/docs/reference/cli/commands.md` is not yours.** It is generated by `tools/clidocs`, which walks the real\n> Cobra tree and emits every command and flag with its Cobra help string. Run `task docs:gen` for it and never edit\n> it by hand. This skill owns the *task-oriented* pages next to it: what each command group is for, what the\n> arguments mean in practice, what the command actually does to the database, and worked examples. Do not try to\n> reproduce an exhaustive flag inventory here — link to `/reference/cli/commands` instead and spend the words on\n> behaviour the help strings cannot express.\n\n## Inputs (source of truth)\n\nRead these in full before writing. Each command group maps to exactly one page.\n\n| Source files (`cli/cmd/`) | Doc page |\n|---------------------------|----------|\n| `root.go` | `docs/docs/reference/cli/README.md` — global flags, command overview |\n| `server.go`, `setup.go`, `version.go` | `docs/docs/reference/cli/README.md` — operational commands |\n| `user.go`, `user_add.go`, `user_change.go`, `user_delete.go`, `user_get.go`, `user_list.go`, `user_token.go`, `user_totp.go` | `docs/docs/reference/cli/users.md` |\n| `project.go`, `project_export.go`, `project_import.go` | `docs/docs/reference/cli/projects.md` |\n| `vault.go`, `vault_check.go`, `vault_rekey.go` | `docs/docs/reference/cli/vaults.md` |\n| `runner.go`, `runner_register.go`, `runner_setup.go`, `runner_start.go`, `runner_unregister.go` | `docs/docs/reference/cli/runners.md` |\n| `migrate.go` | `docs/docs/reference/cli/migrations.md` |\n\n`*_test.go` files (e.g. `runner_register_test.go`, `user_token_test.go`) encode exact behavior — read them to\ndocument precise rules (e.g. registration-token resolution order, token expiry formatting).\n\n`syslog.go` / `syslog_windows.go` are internal helpers, **not** commands — never document them.\n\nRe-discover the command tree before each run; don't trust this table if files were added/removed:\n\n```bash\nls cli/cmd/*.go | grep -v _test\ngrep -rn \"cobra.Command{\" cli/cmd/ | grep -v _test          # every command/subcommand\ngrep -rn \"AddCommand\\|PersistentFlags()\\.\" cli/cmd/          # wiring + flags\n```\n\n## How to read a Cobra command\n\nExtract these facts mechanically from each `*Cmd = &cobra.Command{...}` and its `init()`:\n\n- **Command name** — the `Use:` field (first word). Subcommands are wired with `parent.AddCommand(child)`.\n- **Aliases** — the `Aliases:` slice. Document them (e.g. `users`/`user`, `projects`/`project`,\n  `vaults`/`vault`, `server`/`service`). A command whose `Run` only calls `cmd.Help()` is a pure group — it has\n  no behavior of its own, just subcommands.\n- **Flags** — each `cmd.PersistentFlags().<Type>Var(&arg, \"name\", default, \"usage\")` becomes one table row:\n  flag = `--name`, description from the usage string, and note the default when it's meaningful (e.g.\n  `--enabled` defaults to `true`, `--project-id` defaults to `0` = \"global\"). `StringSliceVar` → \"comma-separated\n  or repeat the flag\".\n- **Required / mutually-exclusive args** — read the `Run` body. Patterns like\n  `if args.x == \"\" { fmt.Println(\"Argument --x required\"); ok = false }` mean **required**; checks that reject\n  setting both of two flags mean **exactly one of**. Document these rules explicitly (\"Exactly one of `--a` or\n  `--b` is required\").\n- **Output** — what the command prints on success (IDs, a table, nothing). Mirror the real `fmt.Printf` format\n  in the doc, including example output blocks for commands that print structured results (e.g. `vault check`).\n\n### Global flags (from `root.go`)\n\nDocumented once in `cli.md`; they apply to every command. Currently: `--config`, `--no-config`, `--log-level`\n(`DEBUG|INFO|WARN|ERROR|FATAL|PANIC`, env `SEMAPHORE_LOG_LEVEL`), `--debug-filter` (DEBUG-only namespace filter,\nenv `SEMAPHORE_DEBUG_FILTER`). Re-read `Execute()` in `root.go` for the live list.\n\n## Page structure (match the existing style)\n\nEvery command-group page follows the shape established in `users.md` and `vaults.md`:\n\n1. `# <Title>` then a one-sentence purpose.\n2. A fenced `semaphore <group> --help` block.\n3. An alias note as a `>` blockquote when the group has an alias (`> \\`vault\\` is an alias for \\`vaults\\`.`).\n4. A subcommand table (`| Command | Purpose |`) with in-page anchor links when the page is long.\n5. One `##` section per subcommand: a short description, a copy-pasteable example, then a\n   `| Flag | Description |` table. State required/exactly-one rules in prose under the table.\n\nConventions:\n\n- Internal links are absolute from the route base `/`: **`/reference/cli/<page>`** (NOT `/cli/<page>` — that\n  base is wrong and 404s). Cross-links to concept pages use the same form, e.g. `/admin-guide/runners`,\n  `/admin-guide/security/encryption`.\n- Admonitions use Docusaurus syntax: `:::tip`, `:::warning` … `:::`.\n- Keep prose task-oriented and match the tone/heading depth of neighboring pages.\n\n## Index page (`cli.md`) and sidebar\n\n- `cli.md` carries: intro + `semaphore help`, a command-group table linking to each page, the **global options**\n  table, and short sections for `version`, `setup`, `server`, `runner`, `migrate`.\n- Every command-group page must be registered in **`docs/sidebars.js`** under the `label: 'CLI'` category's\n  `items` array, as `'reference/cli/<page>'`. Adding a page without registering it hides it from navigation.\n\n## Workflow\n\n### 1. Enumerate the live command tree\nRun the discovery greps above. Build the set of command groups and subcommands that actually exist. Note any\ngroup/subcommand/flag that has no corresponding doc section, and any doc section with no corresponding code.\n\n### 2. Extract facts per command\nFor each command, pull name, aliases, flags (name/default/usage), required/exclusive rules (from `Run`), and\nsuccess output. Cross-check `*_test.go` for exact behavior.\n\n### 3. Update each page\nRewrite or patch each page so it matches the extracted facts and the page structure above. Add sections for new\nsubcommands; remove sections for deleted ones; fix flag tables. Leave a page untouched only after confirming it\nalready matches the code.\n\n### 4. Update the index and sidebar\nRefresh `cli.md` (command table, global options, operational commands) and ensure every page is listed in\n`docs/sidebars.js`.\n\n### 5. Build to catch broken links\nFrom the `docs/` directory:\n\n```bash\ncd docs && npm run build 2>&1 | tail -40\n```\n\nThe build must end with `[SUCCESS] Generated static files`. **Broken-link** errors in pages you touched must be\nfixed. Pre-existing **broken-anchor** warnings in unrelated pages (e.g. `admin-guide/configuration`,\n`admin-guide/installation_manually`) are not yours to fix — note them and move on. If `node_modules` is absent,\nrun `npm install` first (or tell the user the build couldn't run).\n\n## Pitfalls\n\n- **Dead flags.** A flag registered in `init()` but never read in the command's `Run` does nothing — do not\n  document it as functional. Example: after BoltDB removal, `migrate` still registers `--err-log-size`,\n  `--skip-task-output`, `--merge-existing-users`, but `Run` only uses `--apply-to` / `--undo-to`. Verify each\n  flag is actually consumed before documenting it.\n- **Removed features → version-gate, don't silently delete.** When a command/flag is removed (e.g.\n  `migrate --from-boltdb`; BoltDB is \"not supported starting from 2.19\" per `cli/setup/setup.go`), keep the\n  guidance but mark the version boundary with a `:::warning`, so users on older versions are still served. Note\n  when other parts of the repo (e.g. `deployment/docker/server/server-wrapper`) still reference a removed flag —\n  that's a stale call site worth flagging to the user, but out of scope for the docs change.\n- **Stale help strings.** A command's `Short`/`Long` text can lag the code. Verify config-key names against the\n  actual structs in `util/` (e.g. `vault rekey`'s `--old-key` help says `access_key`, but the real config field\n  is `secret_key` / `secret_key_file` in `util/config.go`). Document the struct, not the help string.\n- **Group commands have no flags of their own.** `users`, `projects`, `vaults`, `runner`, `token`, `totp` just\n  print help and dispatch to subcommands. Don't invent flags for them.\n- **Don't document the internal `server` runtime.** `server.go`/`root.go` wire a lot of services; the doc only\n  needs \"start the server, `service` is an alias, respects global flags.\"\n\n## When to ask or refuse\n\n- If a command's behavior is genuinely ambiguous from the code (e.g. a flag is read but its effect spans\n  packages you can't quickly trace), summarize what you found and ask the user rather than guessing.\n- If asked to document a flag or command that **doesn't exist in `cli/cmd/`**, refuse and explain: CLI docs are\n  generated from the Cobra code — add the command/flag first, then regenerate.\n- Before deleting a whole documented section (a removed command group), propose it explicitly; prefer\n  version-gating over silent removal when older releases still ship the feature.\n","frontmatter":{"name":"semaphore-cli-docs","description":"Generate or update the CLI documentation under `docs/docs/reference/cli/` from the Cobra command code in `cli/`. Use this skill whenever the user asks to \"update the CLI docs\", \"actualize/sync the CLI documentation\", \"document the CLI\", \"the CLI docs are out of date\", \"regenerate the runner/user/vault/project/migrate docs\", or after adding or changing a Cobra command, subcommand, flag, alias, or required-argument check anywhere under `cli/cmd/`. Covers every command group (`users`, `projects`, `vaults`, `runner`, `migrate`) plus the operational commands (`server`, `setup`, `version`) and the global flags on the root command. Trigger when the user adds a new `*Cmd` Cobra command, a new `PersistentFlags()` call, or renames/removes a CLI flag.\n"},"isInternal":false,"tokens":2703,"sizeBytes":10686},{"name":"SKILL.md","path":".claude/skills/semaphore-commit/SKILL.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.claude/skills/semaphore-commit/SKILL.md","title":"semaphore-commit","category":"anthropic-skill","format":"markdown","content":"---\nname: semaphore-commit\n---\n\nCreate well-formatted git commits following conventional commit standards.\n\n## Behavior\n1. Analyze staged changes with `git diff --staged`\n2. Generate a conventional commit message\n3. Create the commit with proper formatting\n4. Do not add \"Co-Authored-By: Claude ... <noreply@anthropic.com> to the footer\"\n\n## Commit Format\n```\n<type>(<scope>): <description>\n\n[optional body]\n\n[optional footer]\n```\n\n## Types\n- feat: New feature\n- fix: Bug fix\n- docs: Documentation changes\n- style: Code style changes\n- refactor: Code refactoring\n- test: Adding or modifying tests\n- chore: Maintenance tasks\n- agents: Changes related to AI agents work\n\n## Example Output\n```\nfeat(auth): add password reset functionality\n\n- Add forgot password form\n- Implement email verification flow\n- Add password reset endpoint\n```\n","frontmatter":{"name":"semaphore-commit"},"isInternal":false,"tokens":197,"sizeBytes":834},{"name":"SKILL.md","path":".claude/skills/semaphore-config-schema/SKILL.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.claude/skills/semaphore-config-schema/SKILL.md","title":"semaphore-config-schema","category":"anthropic-skill","format":"markdown","content":"---\nname: semaphore-config-schema\ndescription: >\n  Generate or update the JSON Schema (in YAML form) that describes the Semaphore configuration file. The schema\n  lives at `config.schema.yaml` in the repository root and is derived from the Go struct `util.ConfigType`\n  (with nested types in `util/config.go`, `util/config_auth.go`, `util/App.go`, `util/OdbcProvider.go`). Use this\n  skill whenever the user asks to \"regenerate the config schema\", \"update config.schema.yaml\", \"sync the schema\n  with ConfigType\", \"add a new field to the schema\", \"the schema is out of date\", or after a Go struct change\n  that affects configuration. Also trigger when the user adds a new top-level field, new nested struct, or\n  changes a `json:`, `default:`, `rule:`, or `env:` tag on a config struct field.\n---\n\n# Semaphore — Config Schema Generator\n\nThis skill produces and maintains `config.schema.yaml` — a **JSON Schema draft 2020-12** document (written in\nYAML) describing every valid key in a Semaphore `config.json` / `config.yaml`. The schema is the source of truth\nfor IDE autocomplete (via `yaml-language-server`) and for any future server-side config validation.\n\nThe schema is **derived from Go code**, never hand-edited speculatively. If `ConfigType` doesn't have a field,\nthe schema must not have it. If a field has a `default:` or `rule:` tag, the schema must reflect it.\n\n## Inputs (source of truth)\n\nAll Go types live in the `util` package. Read these files **in full** before writing anything:\n\n| File | What's in it |\n|------|--------------|\n| `util/config.go` | `ConfigType` (root) + most nested types: `DbConfig`, `TLSConfig`, `RunnerConfig`, `TotpConfig`, `EventLogType`, `TaskLogType`, `ConfigLog`, `SyslogConfig`, `ConfigProcess`, `ConfigAppNamespaces`, `ScheduleConfig`, `DebuggingConfig`, `HAConfig`, `HARedisConfig`, `TeamsConfig`, `ConfigDirs`, `SubscriptionConfig`, `LdapMappings`, `oidcEndpoint` |\n| `util/config_auth.go` | `MultifactorAuthConfig`, `EmailAuthConfig`, `TotpConfig`, `RecaptchaConfig` |\n| `util/App.go` | `App` |\n| `util/OdbcProvider.go` | `OidcProvider` (note: filename is `Odbc...`, typo'd — don't \"fix\" it) |\n\nIf a struct moves or a new file appears, find it with:\n\n```bash\ngrep -rn \"^type .* struct\" util/ | grep -v _test\n```\n\n## Output\n\nA single file: **`config.schema.yaml`** in the repository root.\n\nHeader (top of file) — keep exactly this shape:\n\n```yaml\n$schema: \"https://json-schema.org/draft/2020-12/schema\"\n$id: \"https://semaphoreui.com/schemas/config.schema.json\"\ntitle: Semaphore configuration\ndescription: Schema for Semaphore UI config.json / config.yaml. Generated from util.ConfigType.\ntype: object\n```\n\n## Field-mapping rules\n\nThese are non-negotiable — apply them mechanically.\n\n### Property name\n\nUse the **`json:` tag's first segment** (before any comma):\n\n```go\nPort string `json:\"port,omitempty\" ...`   // → property name \"port\"\n```\n\nSkip fields where the `json:` tag is `\"-\"` (e.g. `RunnerConfig.RegistrationToken`, `SubscriptionConfig.Key` —\nthose are env-only, not file-configurable). Document this in the schema's `description` for the containing\nstruct if a sensitive token is reachable only via env.\n\nSkip unexported fields (lowercase first letter) — but include their **referenced types** if they appear via\nexported fields. Example: `oidcEndpoint` is unexported but referenced from `OidcProvider.Endpoint`, so define\nit under `$defs` as `OidcEndpoint`.\n\n### JSON Schema `type`\n\n| Go type | Schema type |\n|---------|-------------|\n| `string` | `string` |\n| `bool` | `boolean` |\n| `int`, `int64`, `uint32` | `integer` |\n| `float64` | `number` |\n| `[]T` | `array`, with `items: { ... }` |\n| `map[string]T` | `object`, with `additionalProperties: { ... }` |\n| `*T` (pointer) | same as `T`. If `*int`, allow `null`: `type: [integer, \"null\"]` |\n| `*Struct` | `{ $ref: \"#/$defs/Struct\" }` |\n\n### Constraints from tags\n\n- **`default:\"X\"`** → emit `default: X` (cast to the right type — `default:\"10\"` on `int` becomes `default: 10`).\n- **`rule:\"^...$\"`** → emit `pattern: \"^...$\"`. **Don't** also add a `format:` — the regex is more authoritative.\n- **`env:\"NAME,sensitive\"`** → append `(sensitive).` to the `description`. The schema doesn't have a native\n  \"sensitive\" flag, so this is documentation-only.\n- **`env:\"NAME\"`** without `sensitive` → no change to schema.\n- URL-looking string fields (web hooks, redirect URLs) → add `format: uri` **unless** a `pattern` is already\n  set from a `rule:` tag.\n\n### `enum` discovery\n\n`enum` is not always in tags — sometimes it's in code:\n\n1. Check for a `rule:\"^a|b|c$\"` regex — convert that to `enum: [a, b, c]` instead of a pattern (more useful in\n   IDE).\n2. Check for typed constants near the struct (e.g. `SyslogFormat` has `SyslogDefault = \"\"` and `SyslogRFC5424\n   = \"rfc5424\"` → `enum: [\"\", rfc5424]`).\n3. `TeamInviteType` constants (`TeamInviteEmail`, `TeamInviteUsername`, `TeamInviteBoth`) → enum.\n\n### Reusable types → `$defs`\n\nEvery named struct that appears as a field type gets a `$defs/<TypeName>` entry. Reference via `$ref:\n\"#/$defs/<TypeName>\"`. Top-level `properties` should reference, not inline.\n\nException: `lumberjack.Logger` is external — define a minimal stub `$defs/LumberjackLogger` with the common\nfields (`filename`, `maxsize`, `maxage`, `maxbackups`, `localtime`, `compress`). Do not try to fully model it.\n\n### `additionalProperties`\n\nLeave it **unset** (defaults to `true`). The schema is informational, not strict — making it strict would\nbreak existing configs that have stray keys (legacy fields, typos used as comments, etc.).\n\n## Workflow\n\n### 1. Read all four Go files\n\nUse `Read` on each file. Do not skim. Note every exported field on every struct.\n\n### 2. Build a mental map: struct → properties\n\nFor `ConfigType` (the root), list every exported field with its `json:` name, Go type, and any `default:` /\n`rule:` / `env:` tags. Then for each referenced struct, do the same. Stop when no new structs appear.\n\n### 3. Diff against existing `config.schema.yaml`\n\nBefore writing, compare what Go has vs. what the schema has:\n\n```bash\n# List all json tag names in Go\ngrep -hoE 'json:\"[^,\"]+' util/*.go | sed 's/json:\"//' | sort -u > /tmp/go-keys.txt\n\n# List all property keys in the schema\nyq '.properties | keys | .[]' config.schema.yaml | sort -u > /tmp/schema-keys.txt\n\ndiff /tmp/go-keys.txt /tmp/schema-keys.txt\n```\n\nIdentify three categories:\n- **In Go, missing from schema** → add.\n- **In schema, missing from Go** → remove (the Go struct is the source of truth).\n- **In both** → check that type / default / pattern still match.\n\nShow the user the diff before applying changes if it's larger than ~5 lines.\n\n### 4. Write `config.schema.yaml`\n\nUse the **YAML formatting conventions** already established in the file:\n\n- Section comments split top-level `properties` into visual groups:\n  `# === Database ===`, `# === HTTP server ===`, `# === Authentication ===`, etc.\n- Inline-object syntax `{ type: string, default: foo }` for one-property entries.\n- Multi-line block form when there are 3+ properties or a `description`.\n- Quote string values that contain `:`, `/`, `+`, `=`, or start with a digit. Don't quote bare identifiers\n  like `string`, `boolean`, `integer`.\n- `$ref` values are quoted: `{ $ref: \"#/$defs/DbConfig\" }`.\n\n### 5. Validate\n\nAfter writing, run two checks:\n\n```bash\npython3 - <<'PY'\nimport yaml, jsonschema\nwith open(\"config.schema.yaml\") as f:\n    schema = yaml.safe_load(f)\njsonschema.Draft202012Validator.check_schema(schema)  # meta-validation\nprint(\"schema is valid\")\n\n# Confirm an existing config.yaml still passes\ntry:\n    with open(\"config.yaml\") as f:\n        cfg = yaml.safe_load(f)\n    errors = list(jsonschema.Draft202012Validator(schema).iter_errors(cfg))\n    print(f\"config.yaml: {len(errors)} error(s)\")\n    for e in errors:\n        print(f\"  {list(e.path)}: {e.message}\")\nexcept FileNotFoundError:\n    pass\nPY\n```\n\nBoth must pass. If `config.yaml` doesn't exist locally, skip the second check — the meta-validation alone is\nsufficient.\n\n### 6. Don't touch `config.yaml`\n\nThe user's `config.yaml` may contain stray legacy keys or comments. Do not \"clean it up\" as part of this\nskill — the schema generation is a one-way operation Go → schema. If the user wants a cleaner config, that's a\nseparate task.\n\n## Common pitfalls\n\n- **`json:\"-\"` fields**: easy to accidentally include. Always check the tag, not the field name.\n- **Pointer vs. value**: `*int` is nullable, plain `int` is not. Reflect that in `type`.\n- **Typo'd filenames**: `util/OdbcProvider.go` (should be `Oidc`) — preserve it; renaming files is out of scope.\n- **Tag inheritance illusion**: nested structs don't inherit tags from their parent field. The `default:` on\n  `ConfigType.MaxParallelTasks` is on the field, not on `int`.\n- **`sensitive` is not standard**: JSON Schema has no built-in sensitive marker. Encode it in `description`\n  text only.\n- **Existing keys with apostrophes** in `config.yaml` (e.g. `\"'email_sender\"`) are user-side comment hacks —\n  ignore them, they're not in any Go struct.\n\n## When to refuse\n\nIf the user asks to add a property to the schema that **doesn't exist in `ConfigType`** (\"just add a `foo`\nfield so my config validates\"), refuse and explain: the schema is generated from Go. The fix is to add the\nfield to the Go struct first, then regenerate.\n\nIf `ConfigType` has been heavily refactored and a previous schema entry no longer maps, propose the deletion\nexplicitly before doing it — never silently drop entries that the user might have been relying on.\n","frontmatter":{"name":"semaphore-config-schema","description":"Generate or update the JSON Schema (in YAML form) that describes the Semaphore configuration file. The schema lives at `config.schema.yaml` in the repository root and is derived from the Go struct `util.ConfigType` (with nested types in `util/config.go`, `util/config_auth.go`, `util/App.go`, `util/OdbcProvider.go`). Use this skill whenever the user asks to \"regenerate the config schema\", \"update config.schema.yaml\", \"sync the schema with ConfigType\", \"add a new field to the schema\", \"the schema is out of date\", or after a Go struct change that affects configuration. Also trigger when the user adds a new top-level field, new nested struct, or changes a `json:`, `default:`, `rule:`, or `env:` tag on a config struct field.\n"},"isInternal":false,"tokens":2523,"sizeBytes":9601},{"name":"license_policy.md","path":".claude/skills/semaphore-third-party-licenses/references/license_policy.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.claude/skills/semaphore-third-party-licenses/references/license_policy.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# License Policy for Semaphore UI\n\nThis is the source of truth for which open-source licenses are acceptable in\nSemaphore UI dependencies. The policy is enforced by\n`scripts/check_policy.py` — if you change anything here, mirror the change\nin that script.\n\nThe policy exists for two reasons:\n\n1. **Customer obligations.** Our MSA commits us to *not* shipping\n   components on terms that would require customers to license or assign\n   their IP. That rules out copyleft licenses with strong reciprocity\n   (GPL family, AGPL, SSPL).\n2. **Project sustainability.** Source-available-but-not-OSS licenses\n   (BUSL, Commons Clause, Elastic 2.0) carry use restrictions that conflict\n   with our open-source distribution model.\n\n## Allowed (no review needed)\n\nThese are permissive licenses with attribution-only obligations. Anything\nunder one of these can be added without further discussion.\n\n| SPDX ID | Name |\n|---------|------|\n| MIT, MIT-0 | MIT License |\n| Apache-2.0 | Apache License 2.0 |\n| BSD-2-Clause | BSD 2-Clause \"Simplified\" |\n| BSD-3-Clause | BSD 3-Clause \"New\" |\n| ISC | ISC License |\n| Zlib | zlib License |\n| Unlicense | The Unlicense |\n| CC0-1.0 | Creative Commons Zero |\n| MPL-2.0 | Mozilla Public License 2.0 (file-level copyleft, OK for libraries) |\n| 0BSD | BSD Zero Clause |\n| BlueOak-1.0.0 | Blue Oak Model License |\n| Python-2.0 | Python Software Foundation License |\n\n**Note on MPL-2.0:** MPL is allowed because its copyleft obligation is at\nthe *file* level, not the project level. We can use MPL-licensed libraries\nwithout making Semaphore UI MPL. If we ever modify an MPL-licensed file,\nthat file must remain MPL.\n\n## Review required\n\nThese licenses are not blanket-forbidden but require a case-by-case\ndecision. Pause and ask before merging a dependency under one of these.\n\n| SPDX ID | Why review |\n|---------|------------|\n| LGPL-2.1, LGPL-3.0 (and `-or-later`) | LGPL is fine for **dynamic** linking, but Go statically links by default. An LGPL Go library would force us to provide object files so users can relink. Acceptable only if the library is genuinely irreplaceable and we can document the relinking pathway. |\n| EPL-1.0, EPL-2.0 | Eclipse Public License has weak copyleft and an explicit patent clause. Generally workable but worth a legal eyeball. |\n| CDDL-1.0, CDDL-1.1 | File-level copyleft like MPL, but with extra requirements. Rare in our ecosystem. |\n| EUPL-1.1, EUPL-1.2 | European Union Public License. Compatible with several other licenses; check the specific version. |\n| OFL-1.1 | SIL Open Font License. Almost always fine for embedded fonts, but verify the font is being used as intended (not redistributed standalone with a new name). |\n\n## Forbidden\n\nA dependency under one of these licenses **must not be added**. If\n`check_policy.py` flags one, the options are: (a) remove it, (b) replace\nit with a permissively-licensed alternative, (c) move the functionality\nout of process (subprocess invocation breaks the linkage in most cases —\nthis is why Ansible is acceptable).\n\n| SPDX ID | Why forbidden |\n|---------|--------------|\n| GPL-2.0, GPL-3.0 (and `-only` / `-or-later`) | Strong copyleft. Linking forces Semaphore UI to be GPL, which conflicts with our distribution model. |\n| AGPL-1.0, AGPL-3.0 | Same as GPL plus a network-interaction trigger. Particularly hostile to SaaS/self-hosted server software. |\n| SSPL-1.0 | MongoDB's Server Side Public License. Not OSI-approved; imposes obligations on anyone offering the software as a service. |\n| BUSL-1.1 | Business Source License. Time-delayed open source with use restrictions during the change window. Not OSS. |\n| Commons-Clause | Restricts commercial sale; strips OSS status from any underlying license it's attached to. |\n| Elastic-2.0 | Elastic License v2. Use restrictions; not OSS. |\n| RSALv2 | Redis Source Available License v2. Same family as BUSL/Elastic. |\n| CC-BY-NC-* | Creative Commons \"Non-Commercial\" variants. Not usable in a commercially-distributable product. |\n| Facebook-BSD-Patents (legacy) | The old React patent grant clause. Removed years ago, but flagged here as a defensive measure. |\n\n## Subprocess exception (External Runtime Tools)\n\nTools that Semaphore UI invokes via `os/exec` (Ansible, Terraform, OpenTofu,\nPulumi, Bash) are NOT linked into our binary and NOT redistributed by us.\nTheir licenses govern the user's installation of those tools on the host\nsystem, not our distribution.\n\nThis is why we can integrate with Ansible (GPL-3.0) and Terraform (BUSL-1.1)\nwithout violating this policy — we never embed their code, we only document\nthat the user must install them separately.\n\nThese tools are intentionally **not listed** in `THIRD-PARTY-LICENSES.md`.\nListing them would wrongly imply that they are distributed with the product\nand would invite unnecessary scrutiny from a customer's legal team. Neither\n`collect_licenses.sh` nor `generate_md.py` produces or accepts an external-\ntools section.\n\n## When the policy itself needs to change\n\nIf a dependency we genuinely need is forbidden, the path forward is:\n\n1. Document the technical need (what does it do, what alternatives exist).\n2. Get sign-off from a maintainer with the authority to make that call.\n3. Update **both** this document and `check_policy.py`, in the same commit.\n4. Note the exception in `THIRD-PARTY-LICENSES.md` so customers can see it.\n\nDon't relax the policy silently. The whole point is that customers can\naudit us, so the policy must be auditable too.\n","isInternal":false,"tokens":1337,"sizeBytes":5481},{"name":"template.md","path":".claude/skills/semaphore-third-party-licenses/references/template.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.claude/skills/semaphore-third-party-licenses/references/template.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# THIRD-PARTY-LICENSES.md output format\n\nThis is the canonical structure that `scripts/generate_md.py` produces.\nReference for humans reviewing the generator output, and for anyone who\nneeds to understand what each section is for.\n\n## Structure\n\n```\n# Third-Party Licenses\n\n[intro paragraph explaining what this file is and why it exists]\n\n_Generated on YYYY-MM-DD HH:MM UTC by scripts/collect_licenses.sh._\n\n[regeneration instructions — always include these so the file is\n reproducible by anyone, not just the original author]\n\n## Summary\n\n[total component count]\n\n| Ecosystem | Components |\n|-----------|------------|\n| Go (backend)        | NN |\n| npm (frontend)      | NN |\n\n### License distribution\n\n| License | Count |\n|---------|-------|\n| MIT          | NN |\n| Apache-2.0   | NN |\n| BSD-3-Clause | NN |\n| ...          | NN |\n\n## Go Backend Dependencies\n\n[one-line description of what these are and where they come from]\n\n| Component | Version(s) | License | Source |\n|-----------|------------|---------|--------|\n| `github.com/foo/bar`  | v1.2.3 | MIT | [link](https://...) |\n| `github.com/foo/baz`  | v0.4.1, v0.5.0 | Apache-2.0 | [link](https://...) |\n| ...\n\n## Frontend Dependencies (npm)\n\n[one-line description]\n\n| Component | Version(s) | License | Source |\n|-----------|------------|---------|--------|\n| `vue`        | 3.4.21 | MIT | [link](https://...) |\n| `vuetify`    | 3.5.8  | MIT | [link](https://...) |\n| ...\n\n---\n\n## License texts\n\n[paragraph explaining where to find full license texts and how to report\n missing/incorrect attributions]\n\n<!-- end of generated file -->\n```\n\n## Why this structure\n\n**Summary table at the top.** Lets a reader (especially a customer's\ncompliance team) see the shape of the dependency tree at a glance\nwithout scrolling through hundreds of rows.\n\n**License distribution.** Surfaces concentration risk. If 95% of deps are\nMIT/Apache and 1 is MPL-2.0, that 1 deserves attention. It also makes it\neasy to spot a forbidden license that somehow slipped through (a single\nGPL-3.0 row in the distribution table is a very loud red flag).\n\n**Sorted alphabetically within each section.** Diffs between releases\nbecome useful — a new dependency shows up as an added row in one\npredictable place, not scattered across the file.\n\n**Versions collapsed onto one line.** A dependency pinned at multiple\nversions (common with transitive Go deps that get MVS-resolved differently\nin different parts of the tree) shouldn't take 5 rows. One row, comma-\nseparated versions.\n\n**External tools deliberately excluded.** Ansible, Terraform, OpenTofu,\nPulumi, and Bash are invoked as subprocesses, not linked, and not\nredistributed with Semaphore UI. They are not listed in this file —\nincluding them would wrongly imply distribution. The license policy\ndocument covers them under \"Subprocess exception\" if a reviewer asks\nwhy they're absent.\n\n**Regeneration instructions in the header.** Means the file is not a\nhand-maintained artifact that drifts. Anyone can rebuild it and verify\nit matches what's actually in the dependency manifests.\n\n**Footer comment marker.** `<!-- end of generated file -->` lets CI\nverify the file wasn't truncated mid-write — useful when generation runs\nin a pipeline and might fail partway.\n\n## What NOT to include\n\n- **Full license texts inline.** Don't copy the MIT license text 200\n  times into one file. Refer readers to the source URL. The packages\n  themselves carry their LICENSE files; the user has them on disk after\n  `go mod download` / `npm install`.\n- **CVE / vulnerability info.** That belongs in security advisories,\n  not the license document. Mixing the two confuses the reader and makes\n  the file go stale faster.\n- **Internal-only dependencies.** Build tooling (linters, test\n  frameworks, code generators) doesn't ship to customers. Production\n  dependencies only.\n- **Hand-edited rows.** If something needs a special note, add it via\n  the generator (extend `generate_md.py`), not by editing the output\n  file. Hand edits get destroyed on the next regeneration.\n","isInternal":false,"tokens":973,"sizeBytes":4050},{"name":"SKILL.md","path":".claude/skills/semaphore-third-party-licenses/SKILL.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/.claude/skills/semaphore-third-party-licenses/SKILL.md","title":"semaphore-third-party-licenses","category":"anthropic-skill","format":"markdown","content":"---\nname: semaphore-third-party-licenses\ndescription: >\n  Generate or update the THIRD-PARTY-LICENSES.md file for the Semaphore UI repository. Use this skill whenever the user \n  asks to create, update, regenerate, audit, or refresh third-party license attributions, OSS notices, SBOM-style \n  license inventories, or NOTICE files for Semaphore UI. Trigger on phrases like \"third-party licenses\", \"OSS notices\",\n  \"license attribution file\", \"SBOM for licenses\", \"NOTICE file\", \"audit dependencies\",\n  \"licenses for go.mod / package.json\", or any request that mentions compliance with customer agreements requiring an\n  OSS components list. Also trigger when the user says \"we added a new dependency, regenerate licenses\". This skill\n  knows the Semaphore UI layout (Go backend + Vue.js frontend), the project's license policy (allowlist/denylist),\n  and the canonical output format.\n---\n\n# Semaphore UI — Third-Party License Generator\n\nThis skill produces `THIRD-PARTY-LICENSES.md` for the Semaphore UI repository. The file lists every open-source\ndependency shipped with Semaphore UI by **name**, **version**, **license**, and **source URL**, grouped by component. It\nexists to satisfy contractual obligations toward customers (identification of OSS components by name, version,\nand license type) and to preserve attribution required by permissive licenses (MIT/BSD/Apache-2.0).\n\n## Repository layout you can expect\n\nSemaphore UI is a self-hosted product. The repository ships:\n\n- **Go backend** — module file at `go.mod`, source under `pkg/`, `services/`, `db/`, etc.\n- **Vue.js frontend** — `web/package.json` (or `web2/package.json` in some branches), built into static assets and\n  embedded in the Go binary.\n\nExternal CLI tools (Ansible, Terraform, OpenTofu, Pulumi, Bash) are invoked at runtime as separate operating-system\nprocesses. They are NOT linked into the binary and NOT redistributed with Semaphore UI, so they are intentionally\n**not listed** in `THIRD-PARTY-LICENSES.md` — their license terms apply to the user who installs them on the host,\nnot to Semaphore UI's distribution. The license policy still discusses why this is contractually safe (see\n`references/license_policy.md`, \"Subprocess exception\").\n\nIf the layout differs (monorepo restructure, new submodule, etc.), inspect the repo first with\n`find . -name \"go.mod\" -o -name \"package.json\" -not -path \"*/node_modules/*\"` and adapt.\n\n## Workflow\n\nFollow these steps in order. Don't skip the policy check — it's the part that actually protects the project from\naccidentally shipping a GPL'd dependency.\n\n### 1. Confirm scope with the user\n\nBefore generating anything, ask (briefly) which targets to include if it's not obvious from context:\n\n- Backend only? Frontend only? Both?\n- Production dependencies only, or also dev dependencies?\n\n**Default**: both backend and frontend, **production-only** (dev dependencies are not distributed to customers and don't\nneed attribution). Override only if the user explicitly asks for dev deps too.\n\n### 2. Collect raw license data\n\nRun `scripts/collect_licenses.sh` from the repo root. It does the following:\n\n- Sets `GOWORK=off` and runs `go-licenses report ./... --template scripts/go_template.tpl` for the Go backend, so\n  workspace siblings (e.g. `pro_impl`) are not pulled in.\n- Rolls sub-package rows up to their parent module (uses `go list -m all` for the module → version map) and drops\n  any module path that belongs to the root module's namespace (first-party packages, including `replace … => ./pro`\n  targets).\n- Applies a small `OVERRIDES` table in the script for modules where `go-licenses` can't auto-detect a license\n  (e.g. `modernc.org/mathutil` → BSD-3-Clause). Extend the table when new \"Unknown\" entries appear.\n- Runs `license-checker --production --json --excludePrivatePackages` in the frontend directory and strips the\n  low-confidence `*` suffix from license strings so `MIT*` collapses into `MIT`.\n- Writes raw output to `.licenses-cache/go.tsv` and `.licenses-cache/npm.json`.\n\nIf the tools aren't installed, the script prints the install commands. Don't try to install them silently — show the\nuser what needs to be added so they can decide whether to add them to CI.\n\n### 3. Run the policy check\n\nRun `scripts/check_policy.py .licenses-cache/`. This compares every detected license against the project policy in\n`references/license_policy.md`:\n\n- **Allowed** (green): MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Zlib, Unlicense, CC0-1.0, MPL-2.0 — proceed.\n- **Review** (yellow): LGPL-2.1, LGPL-3.0, EPL-2.0, CDDL-1.0, custom/unknown — pause and ask the user. LGPL is fine for\n  dynamic linking but Go statically links, so flag it for review.\n- **Forbidden** (red): GPL-2.0, GPL-3.0, AGPL-3.0, SSPL-1.0, BUSL-1.1, Commons Clause, \"non-commercial\" clauses — STOP.\n  Do not proceed with file generation. Report the offending package and ask the user how to handle it (replace, vendor,\n  or accept and document the consequence).\n\n**This step is non-negotiable.** A forbidden license slipping into THIRD-PARTY-LICENSES.md isn't just a documentation\nproblem — it's a contract breach with every customer. If `check_policy.py` returns a  non-zero exit code, surface the \nfailures prominently and stop.\n\n### 4. Generate the markdown\n\nRun `scripts/generate_md.py .licenses-cache/ > THIRD-PARTY-LICENSES.md`. This produces the file using the structure\ndescribed in `references/template.md`. Key points the generator handles:\n\n- Sorts entries alphabetically within each section.\n- Groups by component category (Go backend / npm frontend).\n- Deduplicates packages that appear at multiple versions (lists all versions on one line).\n- Renders a summary table at the top with license counts.\n- Adds a generation timestamp and a note pointing to `scripts/collect_licenses.sh` so the file is reproducible.\n\n### 5. Verify and commit\n\nAfter generation:\n\n1. Diff against the previous `THIRD-PARTY-LICENSES.md` (if it exists). New rows = new dependencies — confirm with the\n   user that they're expected.\n2. Verify the file isn't truncated and ends with the closing footer (`<!-- end of generated file -->`).\n3. Suggest the commit message: `chore(licenses): regenerate THIRD-PARTY-LICENSES.md for vX.Y.Z`.\n4. If a CI pipeline exists (`.github/workflows/`), suggest adding a job that runs `scripts/check_policy.py` on every\n   PR — that's how the project prevents regressions, not by re-checking manually each release.\n\n## Notes on edge cases\n\n**Vendored dependencies.** If the repo has a `vendor/` directory, `go-licenses` will read from it. That's fine —\nvendored code still ships and still needs attribution.\n\n**Dual-licensed packages.** Some packages are offered under \"MIT OR Apache-2.0\". Pick MIT (the simpler one) and note the\nchoice in the entry: `License: MIT (also available under Apache-2.0)`.\n\n**Packages with no detectable license.** `go-licenses` flags these as \"Unknown\". Don't ship the file with Unknowns —\nmanually inspect the repo, fill in the SPDX identifier, and if there's truly no license, treat the package as\nforbidden (no license = \"all rights reserved\", which is incompatible with redistribution).\n\n**Ansible / Terraform / OpenTofu / Pulumi / Bash.** Do NOT list them in `THIRD-PARTY-LICENSES.md`. They are invoked\nas separate OS processes (`os/exec`), not linked, not redistributed. Their licenses bind the user who installs them\non the host, not Semaphore UI's distribution. Listing them would imply otherwise and is an unnecessary signal to a\nreviewing customer's legal team. The \"Subprocess exception\" section in `references/license_policy.md` explains the\nreasoning if a reviewer asks.\n\n**First-party packages with no LICENSE.** The root module and any `replace … => ./<dir>` target (e.g. `./pro`) live\ninside this repo and don't need to be self-attributed. The collection script filters them out by the root module\nprefix from `go.mod`. If you see \"Unknown license\" rows for `github.com/<this-org>/<this-repo>/…`, the filter is\nworking correctly — those rows shouldn't appear in the final cache.\n\n**Embedded license texts.** For packages under MIT/BSD/ISC, the license requires preserving the copyright notice. The\ngenerator embeds the full LICENSE file text from each package's source. For Apache-2.0 packages, it embeds the NOTICE\nfile if one exists.\n\n## What this skill does NOT do\n\n- It does not generate a CycloneDX or SPDX SBOM. If the user wants a machine-readable SBOM (for compliance scanners,\n  customer attestations, etc.), point them at `syft packages dir:. -o cyclonedx-json` as a separate step.\n  THIRD-PARTY-LICENSES.md is the human-readable artifact.\n- It does not check for CVEs. That's a security concern, not a licensing one — use `govulncheck` and `npm audit` for\n  that.\n- It does not modify go.mod or package.json. The skill is read-only with respect to the dependency manifests; it only\n  generates the attribution document.\n\nSee `references/license_policy.md` for the full policy and `references/template.md` for the output format.\n","frontmatter":{"name":"semaphore-third-party-licenses","description":"Generate or update the THIRD-PARTY-LICENSES.md file for the Semaphore UI repository. Use this skill whenever the user  asks to create, update, regenerate, audit, or refresh third-party license attributions, OSS notices, SBOM-style  license inventories, or NOTICE files for Semaphore UI. Trigger on phrases like \"third-party licenses\", \"OSS notices\", \"license attribution file\", \"SBOM for licenses\", \"NOTICE file\", \"audit dependencies\", \"licenses for go.mod / package.json\", or any request that mentions compliance with customer agreements requiring an OSS components list. Also trigger when the user says \"we added a new dependency, regenerate licenses\". This skill knows the Semaphore UI layout (Go backend + Vue.js frontend), the project's license policy (allowlist/denylist), and the canonical output format.\n"},"isInternal":false,"tokens":2162,"sizeBytes":9061},{"name":"README.md","path":"AGENTS/agents/setup/db-query/README.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/AGENTS/agents/setup/db-query/README.md","title":"Subagent: readme","category":"subagent-persona","format":"markdown","content":"# db-query\n\nAd-hoc SQL runners for the project's local databases — **one script per database**. Each runs the\nquery as that database's app user, read-only by default, and logs every query it runs.\n\n| Script | Engine | Mode | Database | `.env` file | `.env` keys | Access |\n| --- | --- | --- | --- | --- | --- | --- |\n| `query_<db>.sh` | PostgreSQL / MariaDB | docker / host | `<database>` | `<path>` | `DB_*` | read-only |\n\n## Setup\n\nNone. Each script reads its connection from the project's own `.env`; the path to it is\n`ENV_FILE` in the script's settings block. Credentials are never copied into this folder.\n\n## Usage\n\n~~~shell\nAGENTS/tools/db-query/query_<db>.sh \"SELECT id, email FROM users LIMIT 5\"\nAGENTS/tools/db-query/query_<db>.sh -f report.sql\necho \"SELECT 1\" | AGENTS/tools/db-query/query_<db>.sh\nAGENTS/tools/db-query/query_<db>.sh \"SELECT ...\" -t -A     # trailing flags go to psql / mariadb\n~~~\n\n## Log\n\nEvery query is appended to `AGENTS/tools/db-query/log/queries.log` before it runs — timestamp,\nscript, target, database, user, access mode, the SQL — and its exit code after. Result rows are not\nlogged. The folder is gitignored.\n\n## Traps\n\n- ⚠️ Local only: a remote docker daemon or a non-loopback host is refused. `ALLOW_REMOTE=1` in a\n  script is set only on the user's explicit request — record who asked and when here.\n- ⚠️ Read-only by default: the session refuses any write (`READ_ONLY=1`). It guards against\n  accidents, not against intent — SQL can switch it off. `READ_ONLY=0` in a script is set only on\n  the user's explicit request — record who asked and when here.\n","isInternal":false,"tokens":429,"sizeBytes":1617},{"name":"INDEX.md","path":"AGENTS/docs/INDEX.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/AGENTS/docs/INDEX.md","title":"Subagent: index","category":"subagent-persona","format":"markdown","content":"# docs — portable standard by zone\n\nApproaches and conventions that would serve another project, not this project's own facts. One\nfolder per zone, each with its own `INDEX.md`. This file routes to a zone; the zone's index routes\nto the doc.\n\n## Zone index (example)\n\nThe rows below are example data, illustrating the shape a real routing table takes; replace them\nonce an actual zone exists.\n\n| When you need it | Read |\n| --- | --- |\n| Example: Writing backend code: models, routes, permissions, entity codes | `backend/INDEX.md` |\n| Example: Running or writing backend tests — mandatory before the first command | `backend/INDEX.md` |\n| Example: Editing the frontend: app layout, import boundaries, i18n | `frontend/INDEX.md` |\n| Example: Setting up a local stack: reverse proxy, HMR behind a proxy, tunnels — the approach, not this project's ports | `infra/INDEX.md` |\n| Example: Editing an env file or another human-read file: grouping, order, language, line length | `standards/INDEX.md` |\n","isInternal":false,"tokens":235,"sizeBytes":1002},{"name":"README.md","path":"AGENTS/tools/wf-stand/README.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/AGENTS/tools/wf-stand/README.md","title":"Subagent: readme","category":"subagent-persona","format":"markdown","content":"# wf-stand — run and verify workflows on the local stand\n\nDrives the agent's throwaway Semaphore stand (`http://localhost:3100`, admin/admin123, project 1,\nPro build from HEAD) to execute a workflow end to end and follow every node to a terminal status.\nExists because a workflow run is asynchronous: tasks, approvals and delays resolve over time and\nonly the run-details endpoint shows all of them together.\n\nCall forms (from the repository root):\n\n```\nAGENTS/tools/wf-stand/stand.sh {start|stop|restart|build|status|log [n]}\nAGENTS/tools/wf-stand/seed.sh\nAGENTS/tools/wf-stand/seed-outputs.sh          # PROJECT_NAME=\"…\" to seed another project\nAGENTS/tools/wf-stand/run.sh <workflow_id> [--approve|--reject] [--stop-after SEC] [--expect success|failed|stopped] [--timeout SEC] [--run ID]\nAGENTS/tools/wf-stand/runner.sh {start|stop|status|tag|untag}   # PID=<project> TAG=<tag>; remote runner for the stand\nAGENTS/tools/wf-stand/inputs.sh <workflow_id> [run_id]           # outputs stored per producer + \"INPUT name=value\" lines per consumer\nNODE_PATH=~/.npm/_npx/9833c18b2d85bc59/node_modules node AGENTS/tools/wf-stand/shoot-run.cjs <workflow_id> [run_id] [out.png] [--dark]\nAGENTS/tools/wf-stand/seed-docs.sh                                # project \"Demo Stand\" + workflow \"Build and deploy\" for the docs screenshots\nAGENTS/tools/wf-stand/seed-survey.sh                              # workflow \"Survey: workflow variables\" (survey + node bindings) in the seed-outputs project\nNODE_PATH=… node AGENTS/tools/wf-stand/shoot-editor.cjs <workflow_id> [node_title] [out.png] [--dark]   # editor with a node selected: survey panel, canvas, bindings\nNODE_PATH=… node AGENTS/tools/wf-stand/shoot-run-dialog.cjs <workflow_id> [out_dir] [--dark]       # starts a run through the survey dialog; shoots the dialog and the run view's Variables strip\nPID=<project> WF=<workflow> NODE_PATH=… node AGENTS/tools/wf-stand/shoot-docs.cjs   # the four screenshots of workflows.md § Outputs and inputs\n```\n\nExamples:\n\n```\n$ AGENTS/tools/wf-stand/stand.sh build && AGENTS/tools/wf-stand/stand.sh start\nstand: up at http://localhost:3100 (pid 40366)\n$ AGENTS/tools/wf-stand/seed.sh\nworkflows happy=5 branches=6                      # ids also in /tmp/semaphore-stand/wf-exec-ids.txt\n$ AGENTS/tools/wf-stand/run.sh 5 --approve --expect success\nrun 4 started (workflow 5) — UI: http://localhost:3100/project/1/workflows/5/runs/4\n17:30:14 approval version=0.1.0 n18/task=task#6:success n19/approval=approval:pending n20/delay=- n21/task=-\n17:30:14 node 19 -> approved\n17:30:24 success version=0.1.0 n18/task=task#6:success n19/approval=approval:approved n20/delay=delay:success n21/task=task#7:success\nfinal: success (run 4)\n$ AGENTS/tools/wf-stand/run.sh 6 --stop-after 8 --expect stopped\nfinal: stopped (run 6)\n```\n\nSeeded workflows: \"Exec happy\" (task → approval → delay 5 s → task, ends `success` with `--approve`)\nand \"Exec branches\" (ok → fail; on_failure → recover, on_success → never started; always → 30 s\nslow task; ends `failed`). Templates are `bash` scripts from the local repo\n`/tmp/semaphore-stand/wf-repo` (`ok.sh`, `fail.sh`, `slow.sh`), so no network or Ansible is needed.\n\n`seed-outputs.sh` seeds the project \"Artifacts QA\" (project 1 since the 2026-10-07 rebuild) for\ntesting workflow outputs by hand: 16 templates (bash producers for every rule of the contract —\nvalid, empty, bad JSON, bad name, 101 outputs, 40 KB value, outputs-then-exit-1, symlink; bash and\nAnsible consumers with one survey var of every type; Terraform / Tofu with a `sensitive` and a\nhyphenated output; Ansible `set_stats`) and four workflows: 1 \"producers -> consumers\" (expect\n`success`, see `task.artifacts` of tasks of nodes 2–4), 2 \"invalid producers\" (expect `failed`,\neach bad producer `error` with the reason in its log, \"empty\" `success` without artifacts),\n3 \"failed producer\" (expect `failed`, no artifacts stored, `on_failure` consumer ran), 4 \"tofu ->\nconsume\" (expect `success`). Scripts: `/tmp/semaphore-stand/wf-repo/produce*.sh`, `consume.sh`,\n`tf/main.tf`, `playbooks/*.yml`. Ids land in `/tmp/semaphore-stand/wf-outputs-ids.txt`.\n\nSince 2026-10-08 the same seed adds the mapping checks: templates \"bash: produce renamed outputs\" and\n\"ansible: set_stats renamed outputs\" (output names `tag`, `count`, `flag`, `subnets`, `cfg`, `envname` that\nmatch no survey variable, so only an explicit edge mapping can deliver them) and workflows 9 \"Inputs: explicit\nmapping\" (renamed producers → bash and ansible consumers over `explicit` edges, `image_tag←tag`,\n`replicas←count`, …), 10 \"Inputs: by_name through approval\" (produce → approval → consumers, run with\n`--approve`), 11 \"Inputs: fan-in by_name vs explicit\" (produce by name and renamed explicitly into one consumer\nwith convergence all: expect `image_tag` from the explicit edge, `replicas` by name), 12 \"Inputs: explicit empty\nlist\" (the edge passes nothing). Consumers print one `INPUT name=value` line per survey variable (bash: parsed\n`k=v` args plus `env:env_name` — a `target: env` variable keeps its own name, it is not upper-cased; ansible: a debug list), so `inputs.sh <wf>` shows producer outputs next to what\nthe consumer received. Expected since the resolver landed (2026-10-08): workflow 1 and 10 — the consumer's\n`INPUT` lines repeat the producer's `image_tag`, `replicas`, `enabled`, `subnet_ids`, `config` (by name;\n`env-name` with a hyphen is not delivered); workflow 9 — the renamed values (`image_tag=renamed-…`,\n`replicas=7`, `env:env_name=prod`); workflow 11 — `image_tag=renamed-…` from the explicit edge and `replicas=3`\nby name; workflow 12 — only `image_tag=static-from-node`, `replicas=1` (the default) and `env:env_name=static-env`;\nworkflow 3 — the on_failure consumer is `error` before start with \"Required input(s) without a value: image_tag\"\nin its log. The task log of every consumer starts with the engine's `Input \"<var>\" <- output \"<key>\" of node N\n(task #M)` lines, so a value's source is always one line above the value.\n\nSince 2026-10-09 the seed also covers the Jinja rule (Ansible never evaluates an expression inside an\nextra var — every value reaches it through a file, the ones with a Jinja2 delimiter tagged `!unsafe`): template \"bash: produce jinja outputs\"\n(`produce_jinja.sh` writes `image_tag`, `config` and `replicas` holding `{{ lookup('pipe', …) }}`,\n`{{ lookup('env', 'HOME') }}`, `{{ 6*7 }}`), the variable group \"vars group with jinja\" (`group_expr: \"{{ 2*21 }}\"`),\ntemplate \"ansible: consume + vars group\" and workflows 13 \"Inputs: jinja injection\" (→ ansible and bash\nconsumers) and 14 \"Inputs: jinja vs vars group\". Expected on the server and on the runner: `INPUT image_tag={{\nlookup('pipe', 'echo INJECTED-$(id -un)') }}`, `config` with the expressions verbatim, `replicas=1` (the\ndefault — `{{ 6*7 }}` is not an int) and `INPUT group_expr={{ 2*21 }}` — the variable group is data too. A\nconsumer log containing `INJECTED-` or `group_expr=42` means Ansible evaluated a value: a regression.\n`inputs.sh` right after `run.sh` on the runner can miss the last log lines; re-run it with the run id.\nTrap: the template API ignores `environment_id`, pass `environment_ids`.\n\nSince 2026-10-10 the stand also checks the secret extra vars (TASK@CEA87DDD08): the survey secret\n`token` and the variable-group secret `db_password` (type `var`, added to \"vars group with jinja\" by hand\nthrough `PUT /project/1/environment/1` with `secrets:[{type:\"var\",name,secret,operation:\"create\"}]`) reach\nAnsible through a second, vault-encrypted `--extra-vars @task_<id>_secret_vars_*.yml` with a one-off\n`--vault-id <guid>@prompt`. `playbooks/consume.yml` prints `INPUT token=…` and `INPUT db_password=…`;\ntemplate 37 \"ansible: consume + own vault\" (access key \"tpl vault\", type login_password, vault `default`)\nruns `playbooks/consume_vault.yml`, which loads `playbooks/vaulted.yml` (encrypted by `ansible-vault` with\nthat key's password) through `vars_files` and prints `INPUT vaulted_var=…`. Expected on the server and on\nthe runner: `INPUT token=s3cr3t {{ 1+1 }}`, `INPUT db_password=p@ss {{ 7*6 }}` literally; `ps -axo args=`\nshows `--vault-id=<guid>@prompt` and two `--extra-vars @`, never a secret or the password; `grep -rl\ns3cr3t /tmp/semaphore-stand/tmp` (or `runner-tmp`) finds nothing while the task runs; no `task_*_vars_*`\nfile is left after. With `SEMAPHORE_LOG_LEVEL=DEBUG SEMAPHORE_DEBUG_FILTER=extra_vars` exported before\n`stand.sh start` / `runner.sh start`, the server and runner logs show seven `context=extra_vars` lines per\ntask (split, two files written, vault added, two files removed, password forgotten). Checked 2026-10-10:\nserver task 162, runner task 164, own vault task 171, regression runs 39 (wf 13) and 40 (wf 14).\nTraps: a vault-encrypted file must not sit in `playbooks/group_vars/` — Ansible loads it for every playbook\nof the directory and a template without that vault fails with \"Decryption failed\"; `vars_files` of the\nrepository are still templated (`{{ 3*3 }}` prints `9`) — only values Semaphore passes are `!unsafe`;\n`runner.sh start | tail` hangs because the runner inherits the pipe — redirect to a file instead; a PUT of a\ntemplate without the `vaults` field deletes its vaults, which is why `runner.sh tag/untag` re-reads each\ntemplate by id before the PUT.\n\nDocs screenshots (2026-10-11): `seed-docs.sh` seeds a separate project \"Demo Stand\" (the name the published\neditor screenshot already shows) with \"Provision network\" (bash, outputs `vpc_id`, `subnet_ids`), \"Build image\"\n(bash, outputs `image_tag`, `replicas` — the values quoted in the user guide), \"Deploy to cluster\" (ansible\n`playbooks/deploy.yml`, survey `image_tag`, `replicas`, `subnets`) and the workflow \"Build and deploy\":\nProvision → Build → Deploy by name plus Provision → Deploy explicit (`subnets ← subnet_ids`). Run it once\n(`PID=<p> run.sh <wf> --expect success`), then `shoot-docs.cjs` writes `workflow-inputs-explicit.png`,\n`workflow-inputs-by-name.png`, `workflow-run-outputs.png` and `task-outputs.png` (dialog clipped) to\n`/tmp/semaphore-stand/shots-docs`; convert with `cwebp -q 88` into `docs/static/assets/`. Traps: a workflow must\nhave exactly one root node (the explicit edge alone would make Provision and Build two roots); key hints need a\nrun of the current revision, so re-run after any re-seed; the editor fits a three-node graph at ~45 % on a 1440 px\nviewport — the script collapses the palette (`.WorkflowEditor__sideToggle`) and clicks the fit control after the\nside panel opens instead of zooming in, which pushes nodes under the panel.\n\nSeeding traps learned there: a Terraform-family template must be POSTed without `inventory_id`\n(the API creates a `terraform-workspace` inventory; a shared one gets claimed and hidden) and with\n`task_params.auto_approve`, or a plan with changes parks the task in `waiting_confirmation`; a\nworkflow must have exactly one root node; `survey_vars[].default_value` is a plain string; a JSON\n`\\\"` inside `$( )` inside a double-quoted bash string is mangled — build such fragments in a\nvariable first.\n\nTraps: `run.sh` exits 1 when `--expect` does not match or `--timeout` (default 180 s) passes;\n`--approve` resolves every pending approval it sees, so use `--run ID` to only follow an existing\nrun. `stand.sh build` rebuilds the Vue app into `api/public` (a tracked-ignored embed input) and\nthe binary at `/tmp/semaphore-stand-bin`; it needs the `pro_impl` workspace for Workflows to be\nenabled. The stand DB is `/tmp/semaphore-stand/database-wf.sqlite` and persists between sessions.\n\n`runner.sh` registers a global runner \"wf-runner\" (tag `wf`, local executor, `/tmp/semaphore-stand/runner-config.json`,\nlog `runner.log`) and starts it from the same binary; `tag` sets `runner_tag` on every template of project\n`PID` (default 1) so their tasks go to it, `untag` sends them back to the server. The remote-runner check\nof workflow outputs (2026-10-08): `runner.sh start && runner.sh tag && run.sh 1 --expect success && run.sh 2\n--expect failed`, then `sqlite3 /tmp/semaphore-stand/database-wf.sqlite \"select id,status,runner_id,length(artifacts)\nfrom task order by id desc limit 13\"` shows `runner_id` of the runner and `artifacts` on the producers only.\nTraps: the token is returned once at creation, so a runner whose config file is gone is deleted and recreated;\nstop the runner and `untag` before a session that expects tasks to run on the server, or they wait forever;\n`status` keeps saying `online` for up to 120 s after `stop` (the server's heartbeat timeout), trust the\n`process:` line.\n\n`seed-survey.sh` (2026-10-16, TASK@B197C72B7F) needs the templates of `seed-outputs.sh` and adds workflow\n\"Survey: workflow variables\": survey `env` (enum, default `staging`), `image_tag` (required), `replicas` (int,\ndefault 2); node 2 \"bash: consume\" bound `image_tag←image_tag`, `replicas←replicas`, `env_name←env` on top of\nstatic `image_tag=static-from-node`; node 3 the same template without bindings (must receive nothing although\nthe names match); node 4 \"bash: produce renamed outputs\" → node 5 \"bash: consume\" over an explicit edge\n`replicas←count` plus the binding `image_tag←image_tag`. Start a run with a body:\n`POST …/workflows/<id>/runs {\"environment\":{\"image_tag\":\"1.2.3\",\"env\":\"prod\"}}` (ids in\n`/tmp/semaphore-stand/wf-survey-ids.txt`). `shoot-editor.cjs` logs in, opens the editor, clicks the first card\nwhose title matches, scrolls the first bound survey field into view and writes the page, a crop of the task\nparams card (`<out>-fields.png`) and the open assign menu (`<out>-assign.png`). `shoot-run-dialog.cjs` opens the\nworkflow page, presses Run, checks that an empty required answer is refused, fills `image_tag` (`IMAGE_TAG`,\ndefault 1.2.3) and the first enum option, starts the run and shoots `run-dialog-invalid.png`, `run-dialog.png` and\n`run-view-variables.png` in `out_dir`; it prints the run id and the Variables chips.\n\n⚠️ 2026-10-16: macOS purges files (not directories) under `/tmp` that were not touched for ~3 days —\n`config-wf.json`, `database-wf.sqlite`, the admin user and every script in `wf-repo` vanished between sessions.\n`stand.sh build` + a fresh `config-wf.json` (sqlite at `$DIR/database-wf.sqlite`, port `:3100`, three\n`openssl rand -base64 32` keys, `use_remote_runner: true`), `migrate`, `user add --admin --login admin\n--password admin123` and the seeds bring it back; `wf-repo` scripts must be recreated before any run.\n","isInternal":false,"tokens":3975,"sizeBytes":14534},{"name":"README.md","path":"AGENTS/work/workflow-editor/mocks/README.md","rawUrl":"https://raw.githubusercontent.com/semaphoreui/semaphore/HEAD/AGENTS/work/workflow-editor/mocks/README.md","title":"Subagent: readme","category":"subagent-persona","format":"markdown","content":"# mocks — approved mock, screenshots and the scripts that shoot them\n\nDesign artefacts of the editor redesign (`TASK@88179c9bc1`); decisions are in\n`../editor-architecture.md`, not here. Every script targets the local stand\n`http://localhost:3100` (admin/admin123, project 1) and Playwright loaded through `NODE_PATH`\nwith `channel: 'chrome'` — stand and Playwright recipes are in `AGENTS/memory/ui-stands.md`.\n\n## Files\n\n| File | What it is |\n| --- | --- |\n| `2026-09-26-workflow-node-mock.html` | Approved static mock: editor canvas, run view, card states, empty state; `#dark` in the URL flips the theme. Open from the repo checkout — MDI icons load from `web/node_modules`. Its \"Tab opens quick-add\" hint is stale (see ⚠️ in `editor-architecture.md`). |\n| `2026-09-26-workflow-node-mock-{light,dark}.png` | Renders of the mock as approved. |\n| `workflow-editor/editor-{light,dark}.png`, `editor-selected-*.png`, `editor-quickadd-light.png`, `editor-legacy-*.png`, `editor-empty-light.png`, `run-{light,dark}.png` | Screenshots of the shipped UI at 1440 × 900 @2x, shot by `shoot-wf.cjs`. \"Legacy\" is a workflow saved without positions (auto layout). |\n| `workflow-editor/editor-interactions.png` | End state of the `interact-wf.cjs` checklist. |\n| `workflow-editor/run-approval-pending.png`, `run-delay-countdown.png` | Run view with a pending approval and the delay countdown, from `approve-wf.cjs`. |\n| `workflow-editor/editor-edge-explicit-{light,dark}.png`, `editor-edge-byname-{light,dark}.png`, `run-outputs-{light,dark}.png`, `task-outputs-{light,dark}.png` | Workflow inputs (2026-10-09, `TASK@1845d9d4d7` stage 7): the edge panel with \"Map inputs explicitly\" and the mapping table (keys suggested from the last run), the by_name panel with the destination's survey variables (a check on those the source produced), the run view with \"N outputs\" on producer cards and the swap icon on explicit pills, the task dialog's Outputs panel. Shot by `shoot-inputs.cjs` on the \"Artifacts QA\" project. |\n| `survey/editor-bindings.png`, `survey/editor-bindings-fields.png`, `survey/editor-bindings-assign.png`, `survey/editor-problem.png` | Workflow survey variables (2026-10-16, `TASK@B197C72B7F` stage 7, redesigned the same day on the owner's request): the editor with the survey in the left panel, \"3 from workflow\" on a bound card and the node panel whose bound survey fields are read-only with a link icon and an unbind cross; a crop of the task params card; the \"assign a workflow variable\" menu open next to an unbound field; the same workflow after the variable \"Image tag\" was deleted — \"2 problems\", Save disabled, badges on the bound nodes and the error under the orphaned field. Shot by `AGENTS/tools/wf-stand/shoot-editor.cjs` on workflow 11 of \"Artifacts QA\" (`seed-survey.sh`). |\n| `survey/run-dialog.png`, `survey/run-dialog-invalid.png`, `survey/run-view-variables.png` | Workflow survey at run time (2026-10-16, `TASK@B197C72B7F` stage 8): the \"Run\" dialog with the survey fields seeded from the defaults, the same dialog refusing an empty required answer, and the run view with the \"Variables\" strip (`run.environment`) under the toolbar. Shot by `AGENTS/tools/wf-stand/shoot-run-dialog.cjs` on workflow 11 of \"Artifacts QA\". |\n| `workflow-editor/editor-revision-active-run.png`, `run-after-edit.png` | Revisions (2026-09-27): the editor with the \"rev. 3\" and \"1 active run\" chips, and run #8 still rendered on its revision 1 after two later saves. Shot with `AGENTS/tools/wf-stand/shoot-run.cjs`. |\n\n## Scripts\n\n| Script | Does | Needs |\n| --- | --- | --- |\n| `seed-wf.sh` | Seeds five templates (one with an XSS name), the \"Release\" workflow with positions, the position-less \"Nightly checks\" (with a `<script>` note), starts one Release run; writes ids to `/tmp/semaphore-stand/wf-ids.txt`. Idempotent for templates. | Running stand, repository 1 and inventory 1 in project 1. |\n| `shoot-wf.cjs` | Editor/run/legacy/empty screenshots in light and dark; output `OUT` (default `/tmp/semaphore-stand/shots-wf`), `RUN` selects the run id. | `wf-ids.txt`. |\n| `interact-wf.cjs` | 16 PASS/FAIL checks on the editor: quick-add from the port, palette click, drag connect, cycle guard, pill condition change, snap, Delete, undo/redo round trip, problem badge + disabled Save, leave guard. Exits 1 on any FAIL or page error. | `wf-ids.txt`. Mutates the Release workflow in the browser but does not save. |\n| `approve-wf.cjs` | Approve inside the card, delay countdown ticks, viewport survives polling, task log opens on click. | `/tmp/semaphore-stand/wf3.txt` with `<workflow id> <run id>` of a run waiting at an approval — seeded by hand, not by `seed-wf.sh`. |\n| `keys-wf.cjs` | Keyboard pan/zoom/fit/reset in the editor and the run view; asserts no effect while typing in the properties panel. | `wf-ids.txt`, run 2 of Release. |\n| `shoot-inputs.cjs` | Shoots the four inputs screenshots above and runs 7 PASS/FAIL checks (checkbox state, mapping rows, hint line, by_name chips with a matched output, output count on a card, Outputs panel in the task dialog). `--dark` for the dark theme; `WF` (default 9, explicit workflow), `WF_BYNAME` (default 1), `OUT` (default `/tmp/semaphore-stand/shots-inputs`). Edges are selected by a dispatched `mousedown` on the path — a real click lands on the condition pill. | `AGENTS/tools/wf-stand/seed-outputs.sh` seeded, a finished run of the current revision of both workflows (`run.sh 9`, `run.sh 1`) for the key hints. |\n| `interact-inputs.cjs` | 10 PASS/FAIL checks of the inputs round trip on workflow `WF` (default 12): select the explicit edge, not dirty before editing, type a key, dirty, explicit icon on the pill, Save → API shows the pair, uncheck → by_name chips, Save → fields gone, original graph restored by PUT. Exits 1 on any FAIL or page error. | Running stand with the seeded project; mutates workflow 12 and restores it. |\n| `shoot-nodes.cjs` | Crops one card per node kind (task hovered with the \"+\", approval, delay, note) with edge pills hidden and zoom 1; source of `docs/static/assets/workflow-node-*.webp`. | `WF` env = id of the \"Docs node kinds\" workflow on the stand (default 4). |\n| `make-hotkeys-svg.py <out.svg>` | Generates the keyboard cheat-sheet `docs/static/assets/workflow-hotkeys.svg`; edit `COLUMNS` and re-run when a shortcut changes. | Python 3 only. |\n","isInternal":false,"tokens":1715,"sizeBytes":6371}],"systemPromptSnippet":"<yakaai_skills repo=\"semaphoreui/semaphore\">\n<!-- File: AGENTS.md (Tokens: ~16 | Category: root-instruction) -->\nRead and follow .claude/CLAUDE.md before starting any work.\n\n<!-- File: .claude/CLAUDE.md (Tokens: ~1184 | Category: claude-rule) -->\n# Claude Code Instructions\n\n# Documentation\n\nThe documentation stored in /docs directory. It is a submodule with official Semaphore UI documentation\nin on Docusaurus engine.\n\nYou should actualize the documentation for new and updated featues.\n\n### Generated reference pages\n\n`docs/docs/reference/configuration.md` and `docs/docs/reference/cli/commands.md` are generated\nfrom this repository and must never be edited by hand:\n\n- `tools/docsref` reads the struct tags and doc comments of `util.ConfigType`.\n- `tools/clidocs` walks the real Cobra command tree from `cli/cmd`.\n\nAfter changing a configuration option or a CLI command, run `task docs:gen` and commit\nthe result together with the code. The `docs-reference` job in `.github/workflows/dev.yml`\nfails when the committed pages differ from what the generators produce.\n\nA config field with no doc comment falls back to `tools/docsref/descriptions.json`.\nPrefer writing the doc comment: `docsref` prints how many options still depend on that\nfile, and the number should only fall.\n\n## Writing Plans\n\nAll plans, tasks, researches for AI agents stored in folder AGENTS.\n\nEach plan has markdown-format and stored in folder AGENTS/plans/<version>.\n\nPlan can be split to tasks. Each task describes in details how to implement some part of some plan.\n\n## Code Style\n\n1. Do not use global variables. Global variables are forbidden.\n\n## High Availability Support\n\nAll solutions must work in High Availability mode.\n\n## Security Is the #1 Priority\n\nAll solutions must be secure by design. Do not consider any solution that introduces security risks.\n\n## How to do research\n\nUse MCP server `research` if your asks you to research something.\n\n## Writing Tests\n\nUse `github.com/stretchr/testify/assert` (and `require` when a failure should stop the test immediately).\n\n### Run tests\n\n```bash\ngo test ./path/to/package/ -run TestFunctionName -v -count=1\n```\n\n### Rules\n\n- Test file goes next to the source: `foo.go` → `foo_test.go`, same package.\n- Use `assert.Equal`, `assert.Empty`, `assert.Nil`, `assert.True`, `assert.Panics`, etc. Never use raw `if`/`t.Fatalf` for assertions.\n- Use `require` (instead of `assert`) when subsequent lines depend on the check passing (e.g., `require.NoError` before using a result).\n- Use table-driven tests with `t.Run` for multiple inputs to the same function.\n- Use `t.TempDir()` for temporary files — cleanup is automatic.\n- When testing code that uses package-level globals (e.g., `util.Config`), initialize them in a helper and reset state between tests.\n- When testing code that reads `os.Stdin`, swap it with `os.Pipe()` and restore in `defer`.\n\n### Template\n\n```go\npackage mypkg\n\nimport (\n    \"testing\"\n\n    \"github.com/stretchr/testify/assert\"\n    \"github.com/stretchr/testify/require\"\n)\n\nfunc TestMyFunction(t *testing.T) {\n    // setup\n    input := \"value\"\n\n    // act\n    result, err := MyFunction(input)\n\n    // assert\n    require.NoError(t, err)\n    assert.Equal(t, \"expected\", result)\n}\n\nfunc TestMyFunction_TableDriven(t *testing.T) {\n    tests := []struct {\n        name     string\n        input    string\n        expected string\n    }{\n        {\"short input\", \"abc\", \"ABC\"},\n        {\"empty input\", \"\", \"\"},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            assert.Equal(t, tt.expected, MyFunction(tt.input))\n        })\n    }\n}\n\nfunc TestMyFunction_Panics(t *testing.T) {\n    assert.Panics(t, func() {\n        MyFunction(nil)\n    })\n}\n```\n\n### Common assertions\n\n| Assertion | Use when |\n|-----------|----------|\n| `assert.Equal(t, expected, actual)` | Comparing values |\n| `assert.NotEqual(t, a, b)` | Values must differ |\n| `assert.Nil(t, val)` / `assert.NotNil(t, val)` | Pointer/interface checks |\n| `assert.Empty(t, val)` | String, slice, or map is zero-length |\n| `assert.Contains(t, str, substr)` | Substring or element in collection |\n| `assert.True(t, cond)` / `assert.False(t, cond)` | Boolean conditions |\n| `assert.NoError(t, err)` / `assert.Error(t, err)` | Error checks |\n| `assert.ErrorContains(t, err, \"msg\")` | Error with specific message |\n| `assert.Panics(t, func(){...})` | Code must panic |\n| `require.NoError(t, err)` | Stop test immediately on error |\n\n### Initializing `util.Config` in tests\n\nMany packages depend on `util.Config` which is a `*ConfigType` pointer (nil by default). Initialize before use:\n\n```go\nfunc setupConfig() {\n    if util.Config == nil {\n        util.Config = &util.ConfigType{}\n    }\n    // Initialize nested pointers as needed:\n    if util.Config.Runner == nil {\n        util.Config.Runner = &util.RunnerConfig{}\n    }\n}\n```\n\n### HTTP handler tests\n\nUse `net/http/httptest`:\n\n```go\nfunc TestMyHandler(t *testing.T) {\n    req := httptest.NewRequest(http.MethodGet, \"/api/endpoint\", nil)\n    w := httptest.NewRecorder()\n\n    myHandler(w, req)\n\n    assert.Equal(t, http.StatusOK, w.Code)\n    assert.Contains(t, w.Body.String(), \"expected\")\n}\n```\n\n<!-- File: CLAUDE.md (Tokens: ~6 | Category: claude-rule) -->\nAGENTS/agent-primary.md\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~1630 | Category: copilot-instructions) -->\n# Semaphore UI Development Instructions\n\n**Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**\n\nSemaphore UI is a modern web interface for managing popular DevOps tools like Ansible, Terraform, PowerShell, and Bash scripts. It's built with Go (backend) and Vue.js (frontend), using Task runner for build automation.\n\n## Working Effectively\n\n### Bootstrap, build, and test the repository:\n- Install Go 1.21+ (currently requires go version 1.21 or higher)\n- Install Node.js 16+ \n- Install Task runner: `go install github.com/go-task/task/v3/cmd/task@latest`\n- Install dependencies: `task deps` -- takes 3 minutes first time (faster with cache). NEVER CANCEL. Set timeout to 5+ minutes.\n- Build the application: `task build` -- takes 1.5 minutes. NEVER CANCEL. Set timeout to 3+ minutes.\n- Run tests: `task test` -- takes 33 seconds. NEVER CANCEL. Set timeout to 2+ minutes.\n\n### Run the application:\n- ALWAYS run the bootstrapping steps first\n- Setup database and admin user: `./bin/semaphore setup` (interactive, use SQLite option 4 for development)\n- Start server: `./bin/semaphore server --config ./config.json`\n- Web UI: http://localhost:3000 (login: admin / changeme)\n- API: http://localhost:3000/api/ (test with: `curl http://localhost:3000/api/ping`)\n\n## Validation\n\n- **CRITICAL**: Always manually validate any new code by building and running the application.\n- ALWAYS run through at least one complete end-to-end scenario after making changes:\n  1. Build the application: `task build`\n  2. Start the server: `./bin/semaphore server --config ./config.json`\n  3. Test API endpoint: `curl http://localhost:3000/api/ping` (should return \"pong\")\n  4. Access web UI at http://localhost:3000 and verify it loads\n  5. For auth changes: Test login with admin/changeme\n- For significant changes, run full setup process to ensure setup still works\n- Always build and exercise your changes before considering the task complete\n\n### Complete Validation Scenario (for major changes):\n```bash\n# 1. Clean build\ntask build\n\n# 2. Setup database (if config.json doesn't exist)\n./bin/semaphore setup  # Choose option 4 (SQLite), use admin/changeme\n\n# 3. Start server\n./bin/semaphore server --config ./config.json\n\n# 4. Test in another terminal\ncurl http://localhost:3000/api/ping  # Should return \"pong\"\ncurl -I http://localhost:3000/       # Should return HTTP 200\n\n# 5. Test web interface manually in browser at http://localhost:3000\n# 6. Test login with admin/changeme if auth-related changes\n```\n\n### Linting and Code Quality\n\n- Frontend linting: `cd web && npm run lint` (has known warnings about console statements and asset sizes - ignore existing issues)\n- Backend linting: `golangci-lint run --timeout=3m` (has known type errors due to module import issues - ignore existing issues) \n- **DO NOT** try to fix existing linting issues unless specifically asked to\n- Always run linting on new code you add to ensure it follows project standards\n- Install golangci-lint if needed: `go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2`\n\n## Common Tasks\n\n### Repository Structure\n```\n.\n├── README.md           - Project documentation  \n├── CONTRIBUTING.md     - Development guidelines\n├── Taskfile.yml       - Task runner configuration\n├── go.mod             - Go module dependencies\n├── web/               - Vue.js frontend application\n│   ├── package.json   - Frontend dependencies\n│   ├── src/           - Vue.js source code\n│   └── public/        - Static assets\n├── cli/               - Go CLI application entry point  \n├── api/               - Go API server endpoints\n├── db/                - Database models and interfaces\n├── services/          - Business logic services\n├── util/              - Utility functions and configuration\n├── bin/               - Built binaries (after build)\n└── config.json       - Runtime configuration (after setup)\n```\n\n### Key Commands Reference\n```bash\n# Install task runner\ngo install github.com/go-task/task/v3/cmd/task@latest\n\n# Install all dependencies (backend + frontend + tools)\ntask deps\n\n# Build application (frontend + backend)\ntask build\n\n# Run tests\ntask test  \n\n# Run linting\ntask lint\n\n# Setup application (interactive)\n./bin/semaphore setup\n\n# Start server\n./bin/semaphore server --config ./config.json\n\n# View available task commands\ntask --list\n```\n\n### Database Options for Development\n\nDuring setup, choose option 4 (SQLite) for the simplest development setup:\n\n- No external database server required\n- Database file stored at the path configured in `config.json` (default under `/tmp/`)\n- Perfect for development and testing\n\n> **Note:** BoltDB (option 2) was removed in Semaphore 2.19. If you have an existing\n> `database.boltdb` file, migrate to SQLite, MySQL, or PostgreSQL before upgrading.\n\n### Frontend Development\n- Vue.js 2.x application in `web/` directory\n- Built with Vue CLI and Vuetify components\n- Build output goes to `api/public/` for serving by Go backend\n- Development server not typically used - Go server serves built assets\n\n### Backend Development  \n- Go application with CLI and API server\n- Uses Gorilla Mux for routing\n- Supports multiple databases: MySQL, PostgreSQL, SQLite (BoltDB was removed in 2.19)\n- Configuration via JSON file or environment variables\n\n## Troubleshooting\n\n### Build Issues\n- If `task` command not found: Install with `go install github.com/go-task/task/v3/cmd/task@latest`\n- If Go version errors: Ensure Go 1.21+ is installed\n- If npm install fails: Ensure Node.js 16+ is installed\n- If build takes too long: This is normal - frontend build can take 60+ seconds\n\n### Runtime Issues  \n- If server won't start: Check config.json exists and database is accessible\n- If web UI shows errors: Check that frontend build completed successfully in `api/public/`\n- If API returns errors: Check server logs for specific error messages\n\n### Database Issues\n\n- For development, use SQLite (option 4) during setup\n- The SQLite database file is created automatically at the configured path\n- If database errors occur, remove the SQLite file and run setup again\n- Configs with `\"dialect\": \"bolt\"` will fail at startup — switch to `sqlite` and re-run setup\n\n## Important Notes\n\n- **NEVER CANCEL** long-running builds or dependency installations\n- Set appropriate timeouts: deps (5+ min), build (3+ min), tests (2+ min)  \n- The application serves the frontend from the Go backend - no separate frontend server needed\n- Configuration is stored in `config.json` after running setup\n- Default admin credentials after setup: admin / changeme\n- Linting has known issues - focus on not introducing new ones\n- Always test changes by running the full application, not just unit tests\n\n<!-- File: .claude/skills/semaphore-checkout/SKILL.md (Tokens: ~484 | Category: anthropic-skill) -->\n---\nname: semaphore-checkout\ndescription: >-\n    Create and switch to a branch with the same name in both repos — the root repo and pro_impl. \n    Usage: /semaphore-checkout <branch_name>\n---\n\nCreate a branch named `<branch_name>` in both repositories at once — the root repo\n(current directory) and the PRO module repo (`./pro_impl`) — and switch both to it.\n\nCI depends on branch names matching: GitHub Actions clones the `pro_impl` branch\nwith the same name as the root repo branch and falls back to `main` if it does not\nexist. This skill keeps the two repos in sync.\n\n## Arguments\n\n- `<branch_name>` (required) — the branch to create. If not provided, ask the user\n  for it. Validate it with `git check-ref-format --branch <branch_name>` before use.\n\n## Behavior\n\nFor each repo — root (`.`) first, then `./pro_impl`:\n\n1. Check the working tree is clean (`git status --porcelain`). If there are\n   uncommitted changes in either repo, stop and report which repo is dirty —\n   do not stash or discard anything.\n2. Fetch the latest state: `git fetch origin`.\n3. Create the branch from the up-to-date default branch and switch to it:\n   - root repo: `git checkout -b <branch_name> origin/develop`\n   - pro_impl: `git checkout -b <branch_name> origin/main`\n4. If the branch already exists locally, just `git checkout <branch_name>`\n   instead of creating it. If it exists only on origin, check it out tracking\n   the remote branch.\n\nDo not push the new branches — they are pushed later with the first commit.\n\n## Rules\n\n- Never force-create (`-B`) or reset an existing branch.\n- Both repos must end up on the same branch name; if step for `pro_impl` fails\n  after the root repo already switched, report the inconsistent state clearly.\n- Report the final state at the end: branch name and start point in each repo.\n\n## Example\n\n```\n/semaphore-checkout fix/task-env-vars\n```\n\nResult:\n\n```\nRoot repo:  fix/task-env-vars (from origin/develop)\npro_impl:   fix/task-env-vars (from origin/main)\n```\n\n<!-- File: .claude/skills/semaphore-cli-docs/SKILL.md (Tokens: ~2703 | Category: anthropic-skill) -->\n---\nname: semaphore-cli-docs\ndescription: >\n  Generate or update the CLI documentation under `docs/docs/reference/cli/` from the Cobra command code in\n  `cli/`. Use this skill whenever the user asks to \"update the CLI docs\", \"actualize/sync the CLI documentation\",\n  \"document the CLI\", \"the CLI docs are out of date\", \"regenerate the runner/user/vault/project/migrate docs\", or\n  after adding or changing a Cobra command, subcommand, flag, alias, or required-argument check anywhere under\n  `cli/cmd/`. Covers every command group (`users`, `projects`, `vaults`, `runner`, `migrate`) plus the operational\n  commands (`server`, `setup`, `version`) and the global flags on the root command. Trigger when the user adds a\n  new `*Cmd` Cobra command, a new `PersistentFlags()` call, or renames/removes a CLI flag.\n---\n\n# Semaphore — CLI Documentation Generator\n\nThis skill produces and maintains the CLI reference pages under\n`docs/docs/reference/cli/` and the CLI index `docs/docs/reference/cli/README.md`. Every page is **derived from the\nCobra command code** in `cli/cmd/`. If a command, subcommand, or flag isn't in the code, it must not be in the\ndocs; if a flag's name, default, or required-ness changed, the docs must change with it.\n\n> `docs/` is a **git submodule** (the documentation site). Editing files under `docs/docs/...` and\n> `docs/sidebars.js` is correct; those changes are committed to the submodule, not the main repo.\n\nThe docs are the human-facing contract. The Go code is the source of truth. When they disagree, the code wins —\n**but** read the command's `Run` body, not just its help strings, because help text drifts (see Pitfalls).\n\n> **`docs/docs/reference/cli/commands.md` is not yours.** It is generated by `tools/clidocs`, which walks the real\n> Cobra tree and emits every command and flag with its Cobra help string. Run `task docs:gen` for it and never edit\n> it by hand. This skill owns the *task-oriented* pages next to it: what each command group is for, what the\n> arguments mean in practice, what the command actually does to the database, and worked examples. Do not try to\n> reproduce an exhaustive flag inventory here — link to `/reference/cli/commands` instead and spend the words on\n> behaviour the help strings cannot express.\n\n## Inputs (source of truth)\n\nRead these in full before writing. Each command group maps to exactly one page.\n\n| Source files (`cli/cmd/`) | Doc page |\n|---------------------------|----------|\n| `root.go` | `docs/docs/reference/cli/README.md` — global flags, command overview |\n| `server.go`, `setup.go`, `version.go` | `docs/docs/reference/cli/README.md` — operational commands |\n| `user.go`, `user_add.go`, `user_change.go`, `user_delete.go`, `user_get.go`, `user_list.go`, `user_token.go`, `user_totp.go` | `docs/docs/reference/cli/users.md` |\n| `project.go`, `project_export.go`, `project_import.go` | `docs/docs/reference/cli/projects.md` |\n| `vault.go`, `vault_check.go`, `vault_rekey.go` | `docs/docs/reference/cli/vaults.md` |\n| `runner.go`, `runner_register.go`, `runner_setup.go`, `runner_start.go`, `runner_unregister.go` | `docs/docs/reference/cli/runners.md` |\n| `migrate.go` | `docs/docs/reference/cli/migrations.md` |\n\n`*_test.go` files (e.g. `runner_register_test.go`, `user_token_test.go`) encode exact behavior — read them to\ndocument precise rules (e.g. registration-token resolution order, token expiry formatting).\n\n`syslog.go` / `syslog_windows.go` are internal helpers, **not** commands — never document them.\n\nRe-discover the command tree before each run; don't trust this table if files were added/removed:\n\n```bash\nls cli/cmd/*.go | grep -v _test\ngrep -rn \"cobra.Command{\" cli/cmd/ | grep -v _test          # every command/subcommand\ngrep -rn \"AddCommand\\|PersistentFlags()\\.\" cli/cmd/          # wiring + flags\n```\n\n## How to read a Cobra command\n\nExtract these facts mechanically from each `*Cmd = &cobra.Command{...}` and its `init()`:\n\n- **Command name** — the `Use:` field (first word). Subcommands are wired with `parent.AddCommand(child)`.\n- **Aliases** — the `Aliases:` slice. Document them (e.g. `users`/`user`, `projects`/`project`,\n  `vaults`/`vault`, `server`/`service`). A command whose `Run` only calls `cmd.Help()` is a pure group — it has\n  no behavior of its own, just subcommands.\n- **Flags** — each `cmd.PersistentFlags().<Type>Var(&arg, \"name\", default, \"usage\")` becomes one table row:\n  flag = `--name`, description from the usage string, and note the default when it's meaningful (e.g.\n  `--enabled` defaults to `true`, `--project-id` defaults to `0` = \"global\"). `StringSliceVar` → \"comma-separated\n  or repeat the flag\".\n- **Required / mutually-exclusive args** — read the `Run` body. Patterns like\n  `if args.x == \"\" { fmt.Println(\"Argument --x required\"); ok = false }` mean **required**; checks that reject\n  setting both of two flags mean **exactly one of**. Document these rules explicitly (\"Exactly one of `--a` or\n  `--b` is required\").\n- **Output** — what the command prints on success (IDs, a table, nothing). Mirror the real `fmt.Printf` format\n  in the doc, including example output blocks for commands that print structured results (e.g. `vault check`).\n\n### Global flags (from `root.go`)\n\nDocumented once in `cli.md`; they apply to every command. Currently: `--config`, `--no-config`, `--log-level`\n(`DEBUG|INFO|WARN|ERROR|FATAL|PANIC`, env `SEMAPHORE_LOG_LEVEL`), `--debug-filter` (DEBUG-only namespace filter,\nenv `SEMAPHORE_DEBUG_FILTER`). Re-read `Execute()` in `root.go` for the live list.\n\n## Page structure (match the existing style)\n\nEvery command-group page follows the shape established in `users.md` and `vaults.md`:\n\n1. `# <Title>` then a one-sentence purpose.\n2. A fenced `semaphore <group> --help` block.\n3. An alias note as a `>` blockquote when the group has an alias (`> \\`vault\\` is an alias for \\`vaults\\`.`).\n4. A subcommand table (`| Command | Purpose |`) with in-page anchor links when the page is long.\n5. One `##` section per subcommand: a short description, a copy-pasteable example, then a\n   `| Flag | Description |` table. State required/exactly-one rules in prose under the table.\n\nConventions:\n\n- Internal links are absolute from the route base `/`: **`/reference/cli/<page>`** (NOT `/cli/<page>` — that\n  base is wrong and 404s). Cross-links to concept pages use the same form, e.g. `/admin-guide/runners`,\n  `/admin-guide/security/encryption`.\n- Admonitions use Docusaurus syntax: `:::tip`, `:::warning` … `:::`.\n- Keep prose task-oriented and match the tone/heading depth of neighboring pages.\n\n## Index page (`cli.md`) and sidebar\n\n- `cli.md` carries: intro + `semaphore help`, a command-group table linking to each page, the **global options**\n  table, and short sections for `version`, `setup`, `server`, `runner`, `migrate`.\n- Every command-group page must be registered in **`docs/sidebars.js`** under the `label: 'CLI'` category's\n  `items` array, as `'reference/cli/<page>'`. Adding a page without registering it hides it from navigation.\n\n## Workflow\n\n### 1. Enumerate the live command tree\nRun the discovery greps above. Build the set of command groups and subcommands that actually exist. Note any\ngroup/subcommand/flag that has no corresponding doc section, and any doc section with no corresponding code.\n\n### 2. Extract facts per command\nFor each command, pull name, aliases, flags (name/default/usage), required/exclusive rules (from `Run`), and\nsuccess output. Cross-check `*_test.go` for exact behavior.\n\n### 3. Update each page\nRewrite or patch each page so it matches the extracted facts and the page structure above. Add sections for new\nsubcommands; remove sections for deleted ones; fix flag tables. Leave a page untouched only after confirming it\nalready matches the code.\n\n### 4. Update the index and sidebar\nRefresh `cli.md` (command table, global options, operational commands) and ensure every page is listed in\n`docs/sidebars.js`.\n\n### 5. Build to catch broken links\nFrom the `docs/` directory:\n\n```bash\ncd docs && npm run build 2>&1 | tail -40\n```\n\nThe build must end with `[SUCCESS] Generated static files`. **Broken-link** errors in pages you touched must be\nfixed. Pre-existing **broken-anchor** warnings in unrelated pages (e.g. `admin-guide/configuration`,\n`admin-guide/installation_manually`) are not yours to fix — note them and move on. If `node_modules` is absent,\nrun `npm install` first (or tell the user the build couldn't run).\n\n## Pitfalls\n\n- **Dead flags.** A flag registered in `init()` but never read in the command's `Run` does nothing — do not\n  document it as functional. Example: after BoltDB removal, `migrate` still registers `--err-log-size`,\n  `--skip-task-output`, `--merge-existing-users`, but `Run` only uses `--apply-to` / `--undo-to`. Verify each\n  flag is actually consumed before documenting it.\n- **Removed features → version-gate, don't silently delete.** When a command/flag is removed (e.g.\n  `migrate --from-boltdb`; BoltDB is \"not supported starting from 2.19\" per `cli/setup/setup.go`), keep the\n  guidance but mark the version boundary with a `:::warning`, so users on older versions are still served. Note\n  when other parts of the repo (e.g. `deployment/docker/server/server-wrapper`) still reference a removed flag —\n  that's a stale call site worth flagging to the user, but out of scope for the docs change.\n- **Stale help strings.** A command's `Short`/`Long` text can lag the code. Verify config-key names against the\n  actual structs in `util/` (e.g. `vault rekey`'s `--old-key` help says `access_key`, but the real config field\n  is `secret_key` / `secret_key_file` in `util/config.go`). Document the struct, not the help string.\n- **Group commands have no flags of their own.** `users`, `projects`, `vaults`, `runner`, `token`, `totp` just\n  print help and dispatch to subcommands. Don't invent flags for them.\n- **Don't document the internal `server` runtime.** `server.go`/`root.go` wire a lot of services; the doc only\n  needs \"start the server, `service` is an alias, respects global flags.\"\n\n## When to ask or refuse\n\n- If a command's behavior is genuinely ambiguous from the code (e.g. a flag is read but its effect spans\n  packages you can't quickly trace), summarize what you found and ask the user rather than guessing.\n- If asked to document a flag or command that **doesn't exist in `cli/cmd/`**, refuse and explain: CLI docs are\n  generated from the Cobra code — add the command/flag first, then regenerate.\n- Before deleting a whole documented section (a removed command group), propose it explicitly; prefer\n  version-gating over silent removal when older releases still ship the feature.\n\n<!-- File: .claude/skills/semaphore-commit/SKILL.md (Tokens: ~197 | Category: anthropic-skill) -->\n---\nname: semaphore-commit\n---\n\nCreate well-formatted git commits following conventional commit standards.\n\n## Behavior\n1. Analyze staged changes with `git diff --staged`\n2. Generate a conventional commit message\n3. Create the commit with proper formatting\n4. Do not add \"Co-Authored-By: Claude ... <noreply@anthropic.com> to the footer\"\n\n## Commit Format\n```\n<type>(<scope>): <description>\n\n[optional body]\n\n[optional footer]\n```\n\n## Types\n- feat: New feature\n- fix: Bug fix\n- docs: Documentation changes\n- style: Code style changes\n- refactor: Code refactoring\n- test: Adding or modifying tests\n- chore: Maintenance tasks\n- agents: Changes related to AI agents work\n\n## Example Output\n```\nfeat(auth): add password reset functionality\n\n- Add forgot password form\n- Implement email verification flow\n- Add password reset endpoint\n```\n\n<!-- File: .claude/skills/semaphore-config-schema/SKILL.md (Tokens: ~2523 | Category: anthropic-skill) -->\n---\nname: semaphore-config-schema\ndescription: >\n  Generate or update the JSON Schema (in YAML form) that describes the Semaphore configuration file. The schema\n  lives at `config.schema.yaml` in the repository root and is derived from the Go struct `util.ConfigType`\n  (with nested types in `util/config.go`, `util/config_auth.go`, `util/App.go`, `util/OdbcProvider.go`). Use this\n  skill whenever the user asks to \"regenerate the config schema\", \"update config.schema.yaml\", \"sync the schema\n  with ConfigType\", \"add a new field to the schema\", \"the schema is out of date\", or after a Go struct change\n  that affects configuration. Also trigger when the user adds a new top-level field, new nested struct, or\n  changes a `json:`, `default:`, `rule:`, or `env:` tag on a config struct field.\n---\n\n# Semaphore — Config Schema Generator\n\nThis skill produces and maintains `config.schema.yaml` — a **JSON Schema draft 2020-12** document (written in\nYAML) describing every valid key in a Semaphore `config.json` / `config.yaml`. The schema is the source of truth\nfor IDE autocomplete (via `yaml-language-server`) and for any future server-side config validation.\n\nThe schema is **derived from Go code**, never hand-edited speculatively. If `ConfigType` doesn't have a field,\nthe schema must not have it. If a field has a `default:` or `rule:` tag, the schema must reflect it.\n\n## Inputs (source of truth)\n\nAll Go types live in the `util` package. Read these files **in full** before writing anything:\n\n| File | What's in it |\n|------|--------------|\n| `util/config.go` | `ConfigType` (root) + most nested types: `DbConfig`, `TLSConfig`, `RunnerConfig`, `TotpConfig`, `EventLogType`, `TaskLogType`, `ConfigLog`, `SyslogConfig`, `ConfigProcess`, `ConfigAppNamespaces`, `ScheduleConfig`, `DebuggingConfig`, `HAConfig`, `HARedisConfig`, `TeamsConfig`, `ConfigDirs`, `SubscriptionConfig`, `LdapMappings`, `oidcEndpoint` |\n| `util/config_auth.go` | `MultifactorAuthConfig`, `EmailAuthConfig`, `TotpConfig`, `RecaptchaConfig` |\n| `util/App.go` | `App` |\n| `util/OdbcProvider.go` | `OidcProvider` (note: filename is `Odbc...`, typo'd — don't \"fix\" it) |\n\nIf a struct moves or a new file appears, find it with:\n\n```bash\ngrep -rn \"^type .* struct\" util/ | grep -v _test\n```\n\n## Output\n\nA single file: **`config.schema.yaml`** in the repository root.\n\nHeader (top of file) — keep exactly this shape:\n\n```yaml\n$schema: \"https://json-schema.org/draft/2020-12/schema\"\n$id: \"https://semaphoreui.com/schemas/config.schema.json\"\ntitle: Semaphore configuration\ndescription: Schema for Semaphore UI config.json / config.yaml. Generated from util.ConfigType.\ntype: object\n```\n\n## Field-mapping rules\n\nThese are non-negotiable — apply them mechanically.\n\n### Property name\n\nUse the **`json:` tag's first segment** (before any comma):\n\n```go\nPort string `json:\"port,omitempty\" ...`   // → property name \"port\"\n```\n\nSkip fields where the `json:` tag is `\"-\"` (e.g. `RunnerConfig.RegistrationToken`, `SubscriptionConfig.Key` —\nthose are env-only, not file-configurable). Document this in the schema's `description` for the containing\nstruct if a sensitive token is reachable only via env.\n\nSkip unexported fields (lowercase first letter) — but include their **referenced types** if they appear via\nexported fields. Example: `oidcEndpoint` is unexported but referenced from `OidcProvider.Endpoint`, so define\nit under `$defs` as `OidcEndpoint`.\n\n### JSON Schema `type`\n\n| Go type | Schema type |\n|---------|-------------|\n| `string` | `string` |\n| `bool` | `boolean` |\n| `int`, `int64`, `uint32` | `integer` |\n| `float64` | `number` |\n| `[]T` | `array`, with `items: { ... }` |\n| `map[string]T` | `object`, with `additionalProperties: { ... }` |\n| `*T` (pointer) | same as `T`. If `*int`, allow `null`: `type: [integer, \"null\"]` |\n| `*Struct` | `{ $ref: \"#/$defs/Struct\" }` |\n\n### Constraints from tags\n\n- **`default:\"X\"`** → emit `default: X` (cast to the right type — `default:\"10\"` on `int` becomes `default: 10`).\n- **`rule:\"^...$\"`** → emit `pattern: \"^...$\"`. **Don't** also add a `format:` — the regex is more authoritative.\n- **`env:\"NAME,sensitive\"`** → append `(sensitive).` to the `description`. The schema doesn't have a native\n  \"sensitive\" flag, so this is documentation-only.\n- **`env:\"NAME\"`** without `sensitive` → no change to schema.\n- URL-looking string fields (web hooks, redirect URLs) → add `format: uri` **unless** a `pattern` is already\n  set from a `rule:` tag.\n\n### `enum` discovery\n\n`enum` is not always in tags — sometimes it's in code:\n\n1. Check for a `rule:\"^a|b|c$\"` regex — convert that to `enum: [a, b, c]` instead of a pattern (more useful in\n   IDE).\n2. Check for typed constants near the struct (e.g. `SyslogFormat` has `SyslogDefault = \"\"` and `SyslogRFC5424\n   = \"rfc5424\"` → `enum: [\"\", rfc5424]`).\n3. `TeamInviteType` constants (`TeamInviteEmail`, `TeamInviteUsername`, `TeamInviteBoth`) → enum.\n\n### Reusable types → `$defs`\n\nEvery named struct that appears as a field type gets a `$defs/<TypeName>` entry. Reference via `$ref:\n\"#/$defs/<TypeName>\"`. Top-level `properties` should reference, not inline.\n\nException: `lumberjack.Logger` is external — define a minimal stub `$defs/LumberjackLogger` with the common\nfields (`filename`, `maxsize`, `maxage`, `maxbackups`, `localtime`, `compress`). Do not try to fully model it.\n\n### `additionalProperties`\n\nLeave it **unset** (defaults to `true`). The schema is informational, not strict — making it strict would\nbreak existing configs that have stray keys (legacy fields, typos used as comments, etc.).\n\n## Workflow\n\n### 1. Read all four Go files\n\nUse `Read` on each file. Do not skim. Note every exported field on every struct.\n\n### 2. Build a mental map: struct → properties\n\nFor `ConfigType` (the root), list every exported field with its `json:` name, Go type, and any `default:` /\n`rule:` / `env:` tags. Then for each referenced struct, do the same. Stop when no new structs appear.\n\n### 3. Diff against existing `config.schema.yaml`\n\nBefore writing, compare what Go has vs. what the schema has:\n\n```bash\n# List all json tag names in Go\ngrep -hoE 'json:\"[^,\"]+' util/*.go | sed 's/json:\"//' | sort -u > /tmp/go-keys.txt\n\n# List all property keys in the schema\nyq '.properties | keys | .[]' config.schema.yaml | sort -u > /tmp/schema-keys.txt\n\ndiff /tmp/go-keys.txt /tmp/schema-keys.txt\n```\n\nIdentify three categories:\n- **In Go, missing from schema** → add.\n- **In schema, missing from Go** → remove (the Go struct is the source of truth).\n- **In both** → check that type / default / pattern still match.\n\nShow the user the diff before applying changes if it's larger than ~5 lines.\n\n### 4. Write `config.schema.yaml`\n\nUse the **YAML formatting conventions** already established in the file:\n\n- Section comments split top-level `properties` into visual groups:\n  `# === Database ===`, `# === HTTP server ===`, `# === Authentication ===`, etc.\n- Inline-object syntax `{ type: string, default: foo }` for one-property entries.\n- Multi-line block form when there are 3+ properties or a `description`.\n- Quote string values that contain `:`, `/`, `+`, `=`, or start with a digit. Don't quote bare identifiers\n  like `string`, `boolean`, `integer`.\n- `$ref` values are quoted: `{ $ref: \"#/$defs/DbConfig\" }`.\n\n### 5. Validate\n\nAfter writing, run two checks:\n\n```bash\npython3 - <<'PY'\nimport yaml, jsonschema\nwith open(\"config.schema.yaml\") as f:\n    schema = yaml.safe_load(f)\njsonschema.Draft202012Validator.check_schema(schema)  # meta-validation\nprint(\"schema is valid\")\n\n# Confirm an existing config.yaml still passes\ntry:\n    with open(\"config.yaml\") as f:\n        cfg = yaml.safe_load(f)\n    errors = list(jsonschema.Draft202012Validator(schema).iter_errors(cfg))\n    print(f\"config.yaml: {len(errors)} error(s)\")\n    for e in errors:\n        print(f\"  {list(e.path)}: {e.message}\")\nexcept FileNotFoundError:\n    pass\nPY\n```\n\nBoth must pass. If `config.yaml` doesn't exist locally, skip the second check — the meta-validation alone is\nsufficient.\n\n### 6. Don't touch `config.yaml`\n\nThe user's `config.yaml` may contain stray legacy keys or comments. Do not \"clean it up\" as part of this\nskill — the schema generation is a one-way operation Go → schema. If the user wants a cleaner config, that's a\nseparate task.\n\n## Common pitfalls\n\n- **`json:\"-\"` fields**: easy to accidentally include. Always check the tag, not the field name.\n- **Pointer vs. value**: `*int` is nullable, plain `int` is not. Reflect that in `type`.\n- **Typo'd filenames**: `util/OdbcProvider.go` (should be `Oidc`) — preserve it; renaming files is out of scope.\n- **Tag inheritance illusion**: nested structs don't inherit tags from their parent field. The `default:` on\n  `ConfigType.MaxParallelTasks` is on the field, not on `int`.\n- **`sensitive` is not standard**: JSON Schema has no built-in sensitive marker. Encode it in `description`\n  text only.\n- **Existing keys with apostrophes** in `config.yaml` (e.g. `\"'email_sender\"`) are user-side comment hacks —\n  ignore them, they're not in any Go struct.\n\n## When to refuse\n\nIf the user asks to add a property to the schema that **doesn't exist in `ConfigType`** (\"just add a `foo`\nfield so my config validates\"), refuse and explain: the schema is generated from Go. The fix is to add the\nfield to the Go struct first, then regenerate.\n\nIf `ConfigType` has been heavily refactored and a previous schema entry no longer maps, propose the deletion\nexplicitly before doing it — never silently drop entries that the user might have been relying on.\n\n<!-- File: .claude/skills/semaphore-third-party-licenses/references/license_policy.md (Tokens: ~1337 | Category: anthropic-skill) -->\n# License Policy for Semaphore UI\n\nThis is the source of truth for which open-source licenses are acceptable in\nSemaphore UI dependencies. The policy is enforced by\n`scripts/check_policy.py` — if you change anything here, mirror the change\nin that script.\n\nThe policy exists for two reasons:\n\n1. **Customer obligations.** Our MSA commits us to *not* shipping\n   components on terms that would require customers to license or assign\n   their IP. That rules out copyleft licenses with strong reciprocity\n   (GPL family, AGPL, SSPL).\n2. **Project sustainability.** Source-available-but-not-OSS licenses\n   (BUSL, Commons Clause, Elastic 2.0) carry use restrictions that conflict\n   with our open-source distribution model.\n\n## Allowed (no review needed)\n\nThese are permissive licenses with attribution-only obligations. Anything\nunder one of these can be added without further discussion.\n\n| SPDX ID | Name |\n|---------|------|\n| MIT, MIT-0 | MIT License |\n| Apache-2.0 | Apache License 2.0 |\n| BSD-2-Clause | BSD 2-Clause \"Simplified\" |\n| BSD-3-Clause | BSD 3-Clause \"New\" |\n| ISC | ISC License |\n| Zlib | zlib License |\n| Unlicense | The Unlicense |\n| CC0-1.0 | Creative Commons Zero |\n| MPL-2.0 | Mozilla Public License 2.0 (file-level copyleft, OK for libraries) |\n| 0BSD | BSD Zero Clause |\n| BlueOak-1.0.0 | Blue Oak Model License |\n| Python-2.0 | Python Software Foundation License |\n\n**Note on MPL-2.0:** MPL is allowed because its copyleft obligation is at\nthe *file* level, not the project level. We can use MPL-licensed libraries\nwithout making Semaphore UI MPL. If we ever modify an MPL-licensed file,\nthat file must remain MPL.\n\n## Review required\n\nThese licenses are not blanket-forbidden but require a case-by-case\ndecision. Pause and ask before merging a dependency under one of these.\n\n| SPDX ID | Why review |\n|---------|------------|\n| LGPL-2.1, LGPL-3.0 (and `-or-later`) | LGPL is fine for **dynamic** linking, but Go statically links by default. An LGPL Go library would force us to provide object files so users can relink. Acceptable only if the library is genuinely irreplaceable and we can document the relinking pathway. |\n| EPL-1.0, EPL-2.0 | Eclipse Public License has weak copyleft and an explicit patent clause. Generally workable but worth a legal eyeball. |\n| CDDL-1.0, CDDL-1.1 | File-level copyleft like MPL, but with extra requirements. Rare in our ecosystem. |\n| EUPL-1.1, EUPL-1.2 | European Union Public License. Compatible with several other licenses; check the specific version. |\n| OFL-1.1 | SIL Open Font License. Almost always fine for embedded fonts, but verify the font is being used as intended (not redistributed standalone with a new name). |\n\n## Forbidden\n\nA dependency under one of these licenses **must not be added**. If\n`check_policy.py` flags one, the options are: (a) remove it, (b) replace\nit with a permissively-licensed alternative, (c) move the functionality\nout of process (subprocess invocation breaks the linkage in most cases —\nthis is why Ansible is acceptable).\n\n| SPDX ID | Why forbidden |\n|---------|--------------|\n| GPL-2.0, GPL-3.0 (and `-only` / `-or-later`) | Strong copyleft. Linking forces Semaphore UI to be GPL, which conflicts with our distribution model. |\n| AGPL-1.0, AGPL-3.0 | Same as GPL plus a network-interaction trigger. Particularly hostile to SaaS/self-hosted server software. |\n| SSPL-1.0 | MongoDB's Server Side Public License. Not OSI-approved; imposes obligations on anyone offering the software as a service. |\n| BUSL-1.1 | Business Source License. Time-delayed open source with use restrictions during the change window. Not OSS. |\n| Commons-Clause | Restricts commercial sale; strips OSS status from any underlying license it's attached to. |\n| Elastic-2.0 | Elastic License v2. Use restrictions; not OSS. |\n| RSALv2 | Redis Source Available License v2. Same family as BUSL/Elastic. |\n| CC-BY-NC-* | Creative Commons \"Non-Commercial\" variants. Not usable in a commercially-distributable product. |\n| Facebook-BSD-Patents (legacy) | The old React patent grant clause. Removed years ago, but flagged here as a defensive measure. |\n\n## Subprocess exception (External Runtime Tools)\n\nTools that Semaphore UI invokes via `os/exec` (Ansible, Terraform, OpenTofu,\nPulumi, Bash) are NOT linked into our binary and NOT redistributed by us.\nTheir licenses govern the user's installation of those tools on the host\nsystem, not our distribution.\n\nThis is why we can integrate with Ansible (GPL-3.0) and Terraform (BUSL-1.1)\nwithout violating this policy — we never embed their code, we only document\nthat the user must install them separately.\n\nThese tools are intentionally **not listed** in `THIRD-PARTY-LICENSES.md`.\nListing them would wrongly imply that they are distributed with the product\nand would invite unnecessary scrutiny from a customer's legal team. Neither\n`collect_licenses.sh` nor `generate_md.py` produces or accepts an external-\ntools section.\n\n## When the policy itself needs to change\n\nIf a dependency we genuinely need is forbidden, the path forward is:\n\n1. Document the technical need (what does it do, what alternatives exist).\n2. Get sign-off from a maintainer with the authority to make that call.\n3. Update **both** this document and `check_policy.py`, in the same commit.\n4. Note the exception in `THIRD-PARTY-LICENSES.md` so customers can see it.\n\nDon't relax the policy silently. The whole point is that customers can\naudit us, so the policy must be auditable too.\n\n<!-- File: .claude/skills/semaphore-third-party-licenses/references/template.md (Tokens: ~973 | Category: anthropic-skill) -->\n# THIRD-PARTY-LICENSES.md output format\n\nThis is the canonical structure that `scripts/generate_md.py` produces.\nReference for humans reviewing the generator output, and for anyone who\nneeds to understand what each section is for.\n\n## Structure\n\n```\n# Third-Party Licenses\n\n[intro paragraph explaining what this file is and why it exists]\n\n_Generated on YYYY-MM-DD HH:MM UTC by scripts/collect_licenses.sh._\n\n[regeneration instructions — always include these so the file is\n reproducible by anyone, not just the original author]\n\n## Summary\n\n[total component count]\n\n| Ecosystem | Components |\n|-----------|------------|\n| Go (backend)        | NN |\n| npm (frontend)      | NN |\n\n### License distribution\n\n| License | Count |\n|---------|-------|\n| MIT          | NN |\n| Apache-2.0   | NN |\n| BSD-3-Clause | NN |\n| ...          | NN |\n\n## Go Backend Dependencies\n\n[one-line description of what these are and where they come from]\n\n| Component | Version(s) | License | Source |\n|-----------|------------|---------|--------|\n| `github.com/foo/bar`  | v1.2.3 | MIT | [link](https://...) |\n| `github.com/foo/baz`  | v0.4.1, v0.5.0 | Apache-2.0 | [link](https://...) |\n| ...\n\n## Frontend Dependencies (npm)\n\n[one-line description]\n\n| Component | Version(s) | License | Source |\n|-----------|------------|---------|--------|\n| `vue`        | 3.4.21 | MIT | [link](https://...) |\n| `vuetify`    | 3.5.8  | MIT | [link](https://...) |\n| ...\n\n---\n\n## License texts\n\n[paragraph explaining where to find full license texts and how to report\n missing/incorrect attributions]\n\n<!-- end of generated file -->\n```\n\n## Why this structure\n\n**Summary table at the top.** Lets a reader (especially a customer's\ncompliance team) see the shape of the dependency tree at a glance\nwithout scrolling through hundreds of rows.\n\n**License distribution.** Surfaces concentration risk. If 95% of deps are\nMIT/Apache and 1 is MPL-2.0, that 1 deserves attention. It also makes it\neasy to spot a forbidden license that somehow slipped through (a single\nGPL-3.0 row in the distribution table is a very loud red flag).\n\n**Sorted alphabetically within each section.** Diffs between releases\nbecome useful — a new dependency shows up as an added row in one\npredictable place, not scattered across the file.\n\n**Versions collapsed onto one line.** A dependency pinned at multiple\nversions (common with transitive Go deps that get MVS-resolved differently\nin different parts of the tree) shouldn't take 5 rows. One row, comma-\nseparated versions.\n\n**External tools deliberately excluded.** Ansible, Terraform, OpenTofu,\nPulumi, and Bash are invoked as subprocesses, not linked, and not\nredistributed with Semaphore UI. They are not listed in this file —\nincluding them would wrongly imply distribution. The license policy\ndocument covers them under \"Subprocess exception\" if a reviewer asks\nwhy they're absent.\n\n**Regeneration instructions in the header.** Means the file is not a\nhand-maintained artifact that drifts. Anyone can rebuild it and verify\nit matches what's actually in the dependency manifests.\n\n**Footer comment marker.** `<!-- end of generated file -->` lets CI\nverify the file wasn't truncated mid-write — useful when generation runs\nin a pipeline and might fail partway.\n\n## What NOT to include\n\n- **Full license texts inline.** Don't copy the MIT license text 200\n  times into one file. Refer readers to the source URL. The packages\n  themselves carry their LICENSE files; the user has them on disk after\n  `go mod download` / `npm install`.\n- **CVE / vulnerability info.** That belongs in security advisories,\n  not the license document. Mixing the two confuses the reader and makes\n  the file go stale faster.\n- **Internal-only dependencies.** Build tooling (linters, test\n  frameworks, code generators) doesn't ship to customers. Production\n  dependencies only.\n- **Hand-edited rows.** If something needs a special note, add it via\n  the generator (extend `generate_md.py`), not by editing the output\n  file. Hand edits get destroyed on the next regeneration.\n\n<!-- File: .claude/skills/semaphore-third-party-licenses/SKILL.md (Tokens: ~2162 | Category: anthropic-skill) -->\n---\nname: semaphore-third-party-licenses\ndescription: >\n  Generate or update the THIRD-PARTY-LICENSES.md file for the Semaphore UI repository. Use this skill whenever the user \n  asks to create, update, regenerate, audit, or refresh third-party license attributions, OSS notices, SBOM-style \n  license inventories, or NOTICE files for Semaphore UI. Trigger on phrases like \"third-party licenses\", \"OSS notices\",\n  \"license attribution file\", \"SBOM for licenses\", \"NOTICE file\", \"audit dependencies\",\n  \"licenses for go.mod / package.json\", or any request that mentions compliance with customer agreements requiring an\n  OSS components list. Also trigger when the user says \"we added a new dependency, regenerate licenses\". This skill\n  knows the Semaphore UI layout (Go backend + Vue.js frontend), the project's license policy (allowlist/denylist),\n  and the canonical output format.\n---\n\n# Semaphore UI — Third-Party License Generator\n\nThis skill produces `THIRD-PARTY-LICENSES.md` for the Semaphore UI repository. The file lists every open-source\ndependency shipped with Semaphore UI by **name**, **version**, **license**, and **source URL**, grouped by component. It\nexists to satisfy contractual obligations toward customers (identification of OSS components by name, version,\nand license type) and to preserve attribution required by permissive licenses (MIT/BSD/Apache-2.0).\n\n## Repository layout you can expect\n\nSemaphore UI is a self-hosted product. The repository ships:\n\n- **Go backend** — module file at `go.mod`, source under `pkg/`, `services/`, `db/`, etc.\n- **Vue.js frontend** — `web/package.json` (or `web2/package.json` in some branches), built into static assets and\n  embedded in the Go binary.\n\nExternal CLI tools (Ansible, Terraform, OpenTofu, Pulumi, Bash) are invoked at runtime as separate operating-system\nprocesses. They are NOT linked into the binary and NOT redistributed with Semaphore UI, so they are intentionally\n**not listed** in `THIRD-PARTY-LICENSES.md` — their license terms apply to the user who installs them on the host,\nnot to Semaphore UI's distribution. The license policy still discusses why this is contractually safe (see\n`references/license_policy.md`, \"Subprocess exception\").\n\nIf the layout differs (monorepo restructure, new submodule, etc.), inspect the repo first with\n`find . -name \"go.mod\" -o -name \"package.json\" -not -path \"*/node_modules/*\"` and adapt.\n\n## Workflow\n\nFollow these steps in order. Don't skip the policy check — it's the part that actually protects the project from\naccidentally shipping a GPL'd dependency.\n\n### 1. Confirm scope with the user\n\nBefore generating anything, ask (briefly) which targets to include if it's not obvious from context:\n\n- Backend only? Frontend only? Both?\n- Production dependencies only, or also dev dependencies?\n\n**Default**: both backend and frontend, **production-only** (dev dependencies are not distributed to customers and don't\nneed attribution). Override only if the user explicitly asks for dev deps too.\n\n### 2. Collect raw license data\n\nRun `scripts/collect_licenses.sh` from the repo root. It does the following:\n\n- Sets `GOWORK=off` and runs `go-licenses report ./... --template scripts/go_template.tpl` for the Go backend, so\n  workspace siblings (e.g. `pro_impl`) are not pulled in.\n- Rolls sub-package rows up to their parent module (uses `go list -m all` for the module → version map) and drops\n  any module path that belongs to the root module's namespace (first-party packages, including `replace … => ./pro`\n  targets).\n- Applies a small `OVERRIDES` table in the script for modules where `go-licenses` can't auto-detect a license\n  (e.g. `modernc.org/mathutil` → BSD-3-Clause). Extend the table when new \"Unknown\" entries appear.\n- Runs `license-checker --production --json --excludePrivatePackages` in the frontend directory and strips the\n  low-confidence `*` suffix from license strings so `MIT*` collapses into `MIT`.\n- Writes raw output to `.licenses-cache/go.tsv` and `.licenses-cache/npm.json`.\n\nIf the tools aren't installed, the script prints the install commands. Don't try to install them silently — show the\nuser what needs to be added so they can decide whether to add them to CI.\n\n### 3. Run the policy check\n\nRun `scripts/check_policy.py .licenses-cache/`. This compares every detected license against the project policy in\n`references/license_policy.md`:\n\n- **Allowed** (green): MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Zlib, Unlicense, CC0-1.0, MPL-2.0 — proceed.\n- **Review** (yellow): LGPL-2.1, LGPL-3.0, EPL-2.0, CDDL-1.0, custom/unknown — pause and ask the user. LGPL is fine for\n  dynamic linking but Go statically links, so flag it for review.\n- **Forbidden** (red): GPL-2.0, GPL-3.0, AGPL-3.0, SSPL-1.0, BUSL-1.1, Commons Clause, \"non-commercial\" clauses — STOP.\n  Do not proceed with file generation. Report the offending package and ask the user how to handle it (replace, vendor,\n  or accept and document the consequence).\n\n**This step is non-negotiable.** A forbidden license slipping into THIRD-PARTY-LICENSES.md isn't just a documentation\nproblem — it's a contract breach with every customer. If `check_policy.py` returns a  non-zero exit code, surface the \nfailures prominently and stop.\n\n### 4. Generate the markdown\n\nRun `scripts/generate_md.py .licenses-cache/ > THIRD-PARTY-LICENSES.md`. This produces the file using the structure\ndescribed in `references/template.md`. Key points the generator handles:\n\n- Sorts entries alphabetically within each section.\n- Groups by component category (Go backend / npm frontend).\n- Deduplicates packages that appear at multiple versions (lists all versions on one line).\n- Renders a summary table at the top with license counts.\n- Adds a generation timestamp and a note pointing to `scripts/collect_licenses.sh` so the file is reproducible.\n\n### 5. Verify and commit\n\nAfter generation:\n\n1. Diff against the previous `THIRD-PARTY-LICENSES.md` (if it exists). New rows = new dependencies — confirm with the\n   user that they're expected.\n2. Verify the file isn't truncated and ends with the closing footer (`<!-- end of generated file -->`).\n3. Suggest the commit message: `chore(licenses): regenerate THIRD-PARTY-LICENSES.md for vX.Y.Z`.\n4. If a CI pipeline exists (`.github/workflows/`), suggest adding a job that runs `scripts/check_policy.py` on every\n   PR — that's how the project prevents regressions, not by re-checking manually each release.\n\n## Notes on edge cases\n\n**Vendored dependencies.** If the repo has a `vendor/` directory, `go-licenses` will read from it. That's fine —\nvendored code still ships and still needs attribution.\n\n**Dual-licensed packages.** Some packages are offered under \"MIT OR Apache-2.0\". Pick MIT (the simpler one) and note the\nchoice in the entry: `License: MIT (also available under Apache-2.0)`.\n\n**Packages with no detectable license.** `go-licenses` flags these as \"Unknown\". Don't ship the file with Unknowns —\nmanually inspect the repo, fill in the SPDX identifier, and if there's truly no license, treat the package as\nforbidden (no license = \"all rights reserved\", which is incompatible with redistribution).\n\n**Ansible / Terraform / OpenTofu / Pulumi / Bash.** Do NOT list them in `THIRD-PARTY-LICENSES.md`. They are invoked\nas separate OS processes (`os/exec`), not linked, not redistributed. Their licenses bind the user who installs them\non the host, not Semaphore UI's distribution. Listing them would imply otherwise and is an unnecessary signal to a\nreviewing customer's legal team. The \"Subprocess exception\" section in `references/license_policy.md` explains the\nreasoning if a reviewer asks.\n\n**First-party packages with no LICENSE.** The root module and any `replace … => ./<dir>` target (e.g. `./pro`) live\ninside this repo and don't need to be self-attributed. The collection script filters them out by the root module\nprefix from `go.mod`. If you see \"Unknown license\" rows for `github.com/<this-org>/<this-repo>/…`, the filter is\nworking correctly — those rows shouldn't appear in the final cache.\n\n**Embedded license texts.** For packages under MIT/BSD/ISC, the license requires preserving the copyright notice. The\ngenerator embeds the full LICENSE file text from each package's source. For Apache-2.0 packages, it embeds the NOTICE\nfile if one exists.\n\n## What this skill does NOT do\n\n- It does not generate a CycloneDX or SPDX SBOM. If the user wants a machine-readable SBOM (for compliance scanners,\n  customer attestations, etc.), point them at `syft packages dir:. -o cyclonedx-json` as a separate step.\n  THIRD-PARTY-LICENSES.md is the human-readable artifact.\n- It does not check for CVEs. That's a security concern, not a licensing one — use `govulncheck` and `npm audit` for\n  that.\n- It does not modify go.mod or package.json. The skill is read-only with respect to the dependency manifests; it only\n  generates the attribution document.\n\nSee `references/license_policy.md` for the full policy and `references/template.md` for the output format.\n\n<!-- File: AGENTS/agents/setup/db-query/README.md (Tokens: ~429 | Category: subagent-persona) -->\n# db-query\n\nAd-hoc SQL runners for the project's local databases — **one script per database**. Each runs the\nquery as that database's app user, read-only by default, and logs every query it runs.\n\n| Script | Engine | Mode | Database | `.env` file | `.env` keys | Access |\n| --- | --- | --- | --- | --- | --- | --- |\n| `query_<db>.sh` | PostgreSQL / MariaDB | docker / host | `<database>` | `<path>` | `DB_*` | read-only |\n\n## Setup\n\nNone. Each script reads its connection from the project's own `.env`; the path to it is\n`ENV_FILE` in the script's settings block. Credentials are never copied into this folder.\n\n## Usage\n\n~~~shell\nAGENTS/tools/db-query/query_<db>.sh \"SELECT id, email FROM users LIMIT 5\"\nAGENTS/tools/db-query/query_<db>.sh -f report.sql\necho \"SELECT 1\" | AGENTS/tools/db-query/query_<db>.sh\nAGENTS/tools/db-query/query_<db>.sh \"SELECT ...\" -t -A     # trailing flags go to psql / mariadb\n~~~\n\n## Log\n\nEvery query is appended to `AGENTS/tools/db-query/log/queries.log` before it runs — timestamp,\nscript, target, database, user, access mode, the SQL — and its exit code after. Result rows are not\nlogged. The folder is gitignored.\n\n## Traps\n\n- ⚠️ Local only: a remote docker daemon or a non-loopback host is refused. `ALLOW_REMOTE=1` in a\n  script is set only on the user's explicit request — record who asked and when here.\n- ⚠️ Read-only by default: the session refuses any write (`READ_ONLY=1`). It guards against\n  accidents, not against intent — SQL can switch it off. `READ_ONLY=0` in a script is set only on\n  the user's explicit request — record who asked and when here.\n\n<!-- File: AGENTS/docs/INDEX.md (Tokens: ~235 | Category: subagent-persona) -->\n# docs — portable standard by zone\n\nApproaches and conventions that would serve another project, not this project's own facts. One\nfolder per zone, each with its own `INDEX.md`. This file routes to a zone; the zone's index routes\nto the doc.\n\n## Zone index (example)\n\nThe rows below are example data, illustrating the shape a real routing table takes; replace them\nonce an actual zone exists.\n\n| When you need it | Read |\n| --- | --- |\n| Example: Writing backend code: models, routes, permissions, entity codes | `backend/INDEX.md` |\n| Example: Running or writing backend tests — mandatory before the first command | `backend/INDEX.md` |\n| Example: Editing the frontend: app layout, import boundaries, i18n | `frontend/INDEX.md` |\n| Example: Setting up a local stack: reverse proxy, HMR behind a proxy, tunnels — the approach, not this project's ports | `infra/INDEX.md` |\n| Example: Editing an env file or another human-read file: grouping, order, language, line length | `standards/INDEX.md` |\n\n<!-- File: AGENTS/tools/wf-stand/README.md (Tokens: ~3975 | Category: subagent-persona) -->\n# wf-stand — run and verify workflows on the local stand\n\nDrives the agent's throwaway Semaphore stand (`http://localhost:3100`, admin/admin123, project 1,\nPro build from HEAD) to execute a workflow end to end and follow every node to a terminal status.\nExists because a workflow run is asynchronous: tasks, approvals and delays resolve over time and\nonly the run-details endpoint shows all of them together.\n\nCall forms (from the repository root):\n\n```\nAGENTS/tools/wf-stand/stand.sh {start|stop|restart|build|status|log [n]}\nAGENTS/tools/wf-stand/seed.sh\nAGENTS/tools/wf-stand/seed-outputs.sh          # PROJECT_NAME=\"…\" to seed another project\nAGENTS/tools/wf-stand/run.sh <workflow_id> [--approve|--reject] [--stop-after SEC] [--expect success|failed|stopped] [--timeout SEC] [--run ID]\nAGENTS/tools/wf-stand/runner.sh {start|stop|status|tag|untag}   # PID=<project> TAG=<tag>; remote runner for the stand\nAGENTS/tools/wf-stand/inputs.sh <workflow_id> [run_id]           # outputs stored per producer + \"INPUT name=value\" lines per consumer\nNODE_PATH=~/.npm/_npx/9833c18b2d85bc59/node_modules node AGENTS/tools/wf-stand/shoot-run.cjs <workflow_id> [run_id] [out.png] [--dark]\nAGENTS/tools/wf-stand/seed-docs.sh                                # project \"Demo Stand\" + workflow \"Build and deploy\" for the docs screenshots\nAGENTS/tools/wf-stand/seed-survey.sh                              # workflow \"Survey: workflow variables\" (survey + node bindings) in the seed-outputs project\nNODE_PATH=… node AGENTS/tools/wf-stand/shoot-editor.cjs <workflow_id> [node_title] [out.png] [--dark]   # editor with a node selected: survey panel, canvas, bindings\nNODE_PATH=… node AGENTS/tools/wf-stand/shoot-run-dialog.cjs <workflow_id> [out_dir] [--dark]       # starts a run through the survey dialog; shoots the dialog and the run view's Variables strip\nPID=<project> WF=<workflow> NODE_PATH=… node AGENTS/tools/wf-stand/shoot-docs.cjs   # the four screenshots of workflows.md § Outputs and inputs\n```\n\nExamples:\n\n```\n$ AGENTS/tools/wf-stand/stand.sh build && AGENTS/tools/wf-stand/stand.sh start\nstand: up at http://localhost:3100 (pid 40366)\n$ AGENTS/tools/wf-stand/seed.sh\nworkflows happy=5 branches=6                      # ids also in /tmp/semaphore-stand/wf-exec-ids.txt\n$ AGENTS/tools/wf-stand/run.sh 5 --approve --expect success\nrun 4 started (workflow 5) — UI: http://localhost:3100/project/1/workflows/5/runs/4\n17:30:14 approval version=0.1.0 n18/task=task#6:success n19/approval=approval:pending n20/delay=- n21/task=-\n17:30:14 node 19 -> approved\n17:30:24 success version=0.1.0 n18/task=task#6:success n19/approval=approval:approved n20/delay=delay:success n21/task=task#7:success\nfinal: success (run 4)\n$ AGENTS/tools/wf-stand/run.sh 6 --stop-after 8 --expect stopped\nfinal: stopped (run 6)\n```\n\nSeeded workflows: \"Exec happy\" (task → approval → delay 5 s → task, ends `success` with `--approve`)\nand \"Exec branches\" (ok → fail; on_failure → recover, on_success → never started; always → 30 s\nslow task; ends `failed`). Templates are `bash` scripts from the local repo\n`/tmp/semaphore-stand/wf-repo` (`ok.sh`, `fail.sh`, `slow.sh`), so no network or Ansible is needed.\n\n`seed-outputs.sh` seeds the project \"Artifacts QA\" (project 1 since the 2026-10-07 rebuild) for\ntesting workflow outputs by hand: 16 templates (bash producers for every rule of the contract —\nvalid, empty, bad JSON, bad name, 101 outputs, 40 KB value, outputs-then-exit-1, symlink; bash and\nAnsible consumers with one survey var of every type; Terraform / Tofu with a `sensitive` and a\nhyphenated output; Ansible `set_stats`) and four workflows: 1 \"producers -> consumers\" (expect\n`success`, see `task.artifacts` of tasks of nodes 2–4), 2 \"invalid producers\" (expect `failed`,\neach bad producer `error` with the reason in its log, \"empty\" `success` without artifacts),\n3 \"failed producer\" (expect `failed`, no artifacts stored, `on_failure` consumer ran), 4 \"tofu ->\nconsume\" (expect `success`). Scripts: `/tmp/semaphore-stand/wf-repo/produce*.sh`, `consume.sh`,\n`tf/main.tf`, `playbooks/*.yml`. Ids land in `/tmp/semaphore-stand/wf-outputs-ids.txt`.\n\nSince 2026-10-08 the same seed adds the mapping checks: templates \"bash: produce renamed outputs\" and\n\"ansible: set_stats renamed outputs\" (output names `tag`, `count`, `flag`, `subnets`, `cfg`, `envname` that\nmatch no survey variable, so only an explicit edge mapping can deliver them) and workflows 9 \"Inputs: explicit\nmapping\" (renamed producers → bash and ansible consumers over `explicit` edges, `image_tag←tag`,\n`replicas←count`, …), 10 \"Inputs: by_name through approval\" (produce → approval → consumers, run with\n`--approve`), 11 \"Inputs: fan-in by_name vs explicit\" (produce by name and renamed explicitly into one consumer\nwith convergence all: expect `image_tag` from the explicit edge, `replicas` by name), 12 \"Inputs: explicit empty\nlist\" (the edge passes nothing). Consumers print one `INPUT name=value` line per survey variable (bash: parsed\n`k=v` args plus `env:env_name` — a `target: env` variable keeps its own name, it is not upper-cased; ansible: a debug list), so `inputs.sh <wf>` shows producer outputs next to what\nthe consumer received. Expected since the resolver landed (2026-10-08): workflow 1 and 10 — the consumer's\n`INPUT` lines repeat the producer's `image_tag`, `replicas`, `enabled`, `subnet_ids`, `config` (by name;\n`env-name` with a hyphen is not delivered); workflow 9 — the renamed values (`image_tag=renamed-…`,\n`replicas=7`, `env:env_name=prod`); workflow 11 — `image_tag=renamed-…` from the explicit edge and `replicas=3`\nby name; workflow 12 — only `image_tag=static-from-node`, `replicas=1` (the default) and `env:env_name=static-env`;\nworkflow 3 — the on_failure consumer is `error` before start with \"Required input(s) without a value: image_tag\"\nin its log. The task log of every consumer starts with the engine's `Input \"<var>\" <- output \"<key>\" of node N\n(task #M)` lines, so a value's source is always one line above the value.\n\nSince 2026-10-09 the seed also covers the Jinja rule (Ansible never evaluates an expression inside an\nextra var — every value reaches it through a file, the ones with a Jinja2 delimiter tagged `!unsafe`): template \"bash: produce jinja outputs\"\n(`produce_jinja.sh` writes `image_tag`, `config` and `replicas` holding `{{ lookup('pipe', …) }}`,\n`{{ lookup('env', 'HOME') }}`, `{{ 6*7 }}`), the variable group \"vars group with jinja\" (`group_expr: \"{{ 2*21 }}\"`),\ntemplate \"ansible: consume + vars group\" and workflows 13 \"Inputs: jinja injection\" (→ ansible and bash\nconsumers) and 14 \"Inputs: jinja vs vars group\". Expected on the server and on the runner: `INPUT image_tag={{\nlookup('pipe', 'echo INJECTED-$(id -un)') }}`, `config` with the expressions verbatim, `replicas=1` (the\ndefault — `{{ 6*7 }}` is not an int) and `INPUT group_expr={{ 2*21 }}` — the variable group is data too. A\nconsumer log containing `INJECTED-` or `group_expr=42` means Ansible evaluated a value: a regression.\n`inputs.sh` right after `run.sh` on the runner can miss the last log lines; re-run it with the run id.\nTrap: the template API ignores `environment_id`, pass `environment_ids`.\n\nSince 2026-10-10 the stand also checks the secret extra vars (TASK@CEA87DDD08): the survey secret\n`token` and the variable-group secret `db_password` (type `var`, added to \"vars group with jinja\" by hand\nthrough `PUT /project/1/environment/1` with `secrets:[{type:\"var\",name,secret,operation:\"create\"}]`) reach\nAnsible through a second, vault-encrypted `--extra-vars @task_<id>_secret_vars_*.yml` with a one-off\n`--vault-id <guid>@prompt`. `playbooks/consume.yml` prints `INPUT token=…` and `INPUT db_password=…`;\ntemplate 37 \"ansible: consume + own vault\" (access key \"tpl vault\", type login_password, vault `default`)\nruns `playbooks/consume_vault.yml`, which loads `playbooks/vaulted.yml` (encrypted by `ansible-vault` with\nthat key's password) through `vars_files` and prints `INPUT vaulted_var=…`. Expected on the server and on\nthe runner: `INPUT token=s3cr3t {{ 1+1 }}`, `INPUT db_password=p@ss {{ 7*6 }}` literally; `ps -axo args=`\nshows `--vault-id=<guid>@prompt` and two `--extra-vars @`, never a secret or the password; `grep -rl\ns3cr3t /tmp/semaphore-stand/tmp` (or `runner-tmp`) finds nothing while the task runs; no `task_*_vars_*`\nfile is left after. With `SEMAPHORE_LOG_LEVEL=DEBUG SEMAPHORE_DEBUG_FILTER=extra_vars` exported before\n`stand.sh start` / `runner.sh start`, the server and runner logs show seven `context=extra_vars` lines per\ntask (split, two files written, vault added, two files removed, password forgotten). Checked 2026-10-10:\nserver task 162, runner task 164, own vault task 171, regression runs 39 (wf 13) and 40 (wf 14).\nTraps: a vault-encrypted file must not sit in `playbooks/group_vars/` — Ansible loads it for every playbook\nof the directory and a template without that vault fails with \"Decryption failed\"; `vars_files` of the\nrepository are still templated (`{{ 3*3 }}` prints `9`) — only values Semaphore passes are `!unsafe`;\n`runner.sh start | tail` hangs because the runner inherits the pipe — redirect to a file instead; a PUT of a\ntemplate without the `vaults` field deletes its vaults, which is why `runner.sh tag/untag` re-reads each\ntemplate by id before the PUT.\n\nDocs screenshots (2026-10-11): `seed-docs.sh` seeds a separate project \"Demo Stand\" (the name the published\neditor screenshot already shows) with \"Provision network\" (bash, outputs `vpc_id`, `subnet_ids`), \"Build image\"\n(bash, outputs `image_tag`, `replicas` — the values quoted in the user guide), \"Deploy to cluster\" (ansible\n`playbooks/deploy.yml`, survey `image_tag`, `replicas`, `subnets`) and the workflow \"Build and deploy\":\nProvision → Build → Deploy by name plus Provision → Deploy explicit (`subnets ← subnet_ids`). Run it once\n(`PID=<p> run.sh <wf> --expect success`), then `shoot-docs.cjs` writes `workflow-inputs-explicit.png`,\n`workflow-inputs-by-name.png`, `workflow-run-outputs.png` and `task-outputs.png` (dialog clipped) to\n`/tmp/semaphore-stand/shots-docs`; convert with `cwebp -q 88` into `docs/static/assets/`. Traps: a workflow must\nhave exactly one root node (the explicit edge alone would make Provision and Build two roots); key hints need a\nrun of the current revision, so re-run after any re-seed; the editor fits a three-node graph at ~45 % on a 1440 px\nviewport — the script collapses the palette (`.WorkflowEditor__sideToggle`) and clicks the fit control after the\nside panel opens instead of zooming in, which pushes nodes under the panel.\n\nSeeding traps learned there: a Terraform-family template must be POSTed without `inventory_id`\n(the API creates a `terraform-workspace` inventory; a shared one gets claimed and hidden) and with\n`task_params.auto_approve`, or a plan with changes parks the task in `waiting_confirmation`; a\nworkflow must have exactly one root node; `survey_vars[].default_value` is a plain string; a JSON\n`\\\"` inside `$( )` inside a double-quoted bash string is mangled — build such fragments in a\nvariable first.\n\nTraps: `run.sh` exits 1 when `--expect` does not match or `--timeout` (default 180 s) passes;\n`--approve` resolves every pending approval it sees, so use `--run ID` to only follow an existing\nrun. `stand.sh build` rebuilds the Vue app into `api/public` (a tracked-ignored embed input) and\nthe binary at `/tmp/semaphore-stand-bin`; it needs the `pro_impl` workspace for Workflows to be\nenabled. The stand DB is `/tmp/semaphore-stand/database-wf.sqlite` and persists between sessions.\n\n`runner.sh` registers a global runner \"wf-runner\" (tag `wf`, local executor, `/tmp/semaphore-stand/runner-config.json`,\nlog `runner.log`) and starts it from the same binary; `tag` sets `runner_tag` on every template of project\n`PID` (default 1) so their tasks go to it, `untag` sends them back to the server. The remote-runner check\nof workflow outputs (2026-10-08): `runner.sh start && runner.sh tag && run.sh 1 --expect success && run.sh 2\n--expect failed`, then `sqlite3 /tmp/semaphore-stand/database-wf.sqlite \"select id,status,runner_id,length(artifacts)\nfrom task order by id desc limit 13\"` shows `runner_id` of the runner and `artifacts` on the producers only.\nTraps: the token is returned once at creation, so a runner whose config file is gone is deleted and recreated;\nstop the runner and `untag` before a session that expects tasks to run on the server, or they wait forever;\n`status` keeps saying `online` for up to 120 s after `stop` (the server's heartbeat timeout), trust the\n`process:` line.\n\n`seed-survey.sh` (2026-10-16, TASK@B197C72B7F) needs the templates of `seed-outputs.sh` and adds workflow\n\"Survey: workflow variables\": survey `env` (enum, default `staging`), `image_tag` (required), `replicas` (int,\ndefault 2); node 2 \"bash: consume\" bound `image_tag←image_tag`, `replicas←replicas`, `env_name←env` on top of\nstatic `image_tag=static-from-node`; node 3 the same template without bindings (must receive nothing although\nthe names match); node 4 \"bash: produce renamed outputs\" → node 5 \"bash: consume\" over an explicit edge\n`replicas←count` plus the binding `image_tag←image_tag`. Start a run with a body:\n`POST …/workflows/<id>/runs {\"environment\":{\"image_tag\":\"1.2.3\",\"env\":\"prod\"}}` (ids in\n`/tmp/semaphore-stand/wf-survey-ids.txt`). `shoot-editor.cjs` logs in, opens the editor, clicks the first card\nwhose title matches, scrolls the first bound survey field into view and writes the page, a crop of the task\nparams card (`<out>-fields.png`) and the open assign menu (`<out>-assign.png`). `shoot-run-dialog.cjs` opens the\nworkflow page, presses Run, checks that an empty required answer is refused, fills `image_tag` (`IMAGE_TAG`,\ndefault 1.2.3) and the first enum option, starts the run and shoots `run-dialog-invalid.png`, `run-dialog.png` and\n`run-view-variables.png` in `out_dir`; it prints the run id and the Variables chips.\n\n⚠️ 2026-10-16: macOS purges files (not directories) under `/tmp` that were not touched for ~3 days —\n`config-wf.json`, `database-wf.sqlite`, the admin user and every script in `wf-repo` vanished between sessions.\n`stand.sh build` + a fresh `config-wf.json` (sqlite at `$DIR/database-wf.sqlite`, port `:3100`, three\n`openssl rand -base64 32` keys, `use_remote_runner: true`), `migrate`, `user add --admin --login admin\n--password admin123` and the seeds bring it back; `wf-repo` scripts must be recreated before any run.\n\n<!-- File: AGENTS/work/workflow-editor/mocks/README.md (Tokens: ~1715 | Category: subagent-persona) -->\n# mocks — approved mock, screenshots and the scripts that shoot them\n\nDesign artefacts of the editor redesign (`TASK@88179c9bc1`); decisions are in\n`../editor-architecture.md`, not here. Every script targets the local stand\n`http://localhost:3100` (admin/admin123, project 1) and Playwright loaded through `NODE_PATH`\nwith `channel: 'chrome'` — stand and Playwright recipes are in `AGENTS/memory/ui-stands.md`.\n\n## Files\n\n| File | What it is |\n| --- | --- |\n| `2026-09-26-workflow-node-mock.html` | Approved static mock: editor canvas, run view, card states, empty state; `#dark` in the URL flips the theme. Open from the repo checkout — MDI icons load from `web/node_modules`. Its \"Tab opens quick-add\" hint is stale (see ⚠️ in `editor-architecture.md`). |\n| `2026-09-26-workflow-node-mock-{light,dark}.png` | Renders of the mock as approved. |\n| `workflow-editor/editor-{light,dark}.png`, `editor-selected-*.png`, `editor-quickadd-light.png`, `editor-legacy-*.png`, `editor-empty-light.png`, `run-{light,dark}.png` | Screenshots of the shipped UI at 1440 × 900 @2x, shot by `shoot-wf.cjs`. \"Legacy\" is a workflow saved without positions (auto layout). |\n| `workflow-editor/editor-interactions.png` | End state of the `interact-wf.cjs` checklist. |\n| `workflow-editor/run-approval-pending.png`, `run-delay-countdown.png` | Run view with a pending approval and the delay countdown, from `approve-wf.cjs`. |\n| `workflow-editor/editor-edge-explicit-{light,dark}.png`, `editor-edge-byname-{light,dark}.png`, `run-outputs-{light,dark}.png`, `task-outputs-{light,dark}.png` | Workflow inputs (2026-10-09, `TASK@1845d9d4d7` stage 7): the edge panel with \"Map inputs explicitly\" and the mapping table (keys suggested from the last run), the by_name panel with the destination's survey variables (a check on those the source produced), the run view with \"N outputs\" on producer cards and the swap icon on explicit pills, the task dialog's Outputs panel. Shot by `shoot-inputs.cjs` on the \"Artifacts QA\" project. |\n| `survey/editor-bindings.png`, `survey/editor-bindings-fields.png`, `survey/editor-bindings-assign.png`, `survey/editor-problem.png` | Workflow survey variables (2026-10-16, `TASK@B197C72B7F` stage 7, redesigned the same day on the owner's request): the editor with the survey in the left panel, \"3 from workflow\" on a bound card and the node panel whose bound survey fields are read-only with a link icon and an unbind cross; a crop of the task params card; the \"assign a workflow variable\" menu open next to an unbound field; the same workflow after the variable \"Image tag\" was deleted — \"2 problems\", Save disabled, badges on the bound nodes and the error under the orphaned field. Shot by `AGENTS/tools/wf-stand/shoot-editor.cjs` on workflow 11 of \"Artifacts QA\" (`seed-survey.sh`). |\n| `survey/run-dialog.png`, `survey/run-dialog-invalid.png`, `survey/run-view-variables.png` | Workflow survey at run time (2026-10-16, `TASK@B197C72B7F` stage 8): the \"Run\" dialog with the survey fields seeded from the defaults, the same dialog refusing an empty required answer, and the run view with the \"Variables\" strip (`run.environment`) under the toolbar. Shot by `AGENTS/tools/wf-stand/shoot-run-dialog.cjs` on workflow 11 of \"Artifacts QA\". |\n| `workflow-editor/editor-revision-active-run.png`, `run-after-edit.png` | Revisions (2026-09-27): the editor with the \"rev. 3\" and \"1 active run\" chips, and run #8 still rendered on its revision 1 after two later saves. Shot with `AGENTS/tools/wf-stand/shoot-run.cjs`. |\n\n## Scripts\n\n| Script | Does | Needs |\n| --- | --- | --- |\n| `seed-wf.sh` | Seeds five templates (one with an XSS name), the \"Release\" workflow with positions, the position-less \"Nightly checks\" (with a `<script>` note), starts one Release run; writes ids to `/tmp/semaphore-stand/wf-ids.txt`. Idempotent for templates. | Running stand, repository 1 and inventory 1 in project 1. |\n| `shoot-wf.cjs` | Editor/run/legacy/empty screenshots in light and dark; output `OUT` (default `/tmp/semaphore-stand/shots-wf`), `RUN` selects the run id. | `wf-ids.txt`. |\n| `interact-wf.cjs` | 16 PASS/FAIL checks on the editor: quick-add from the port, palette click, drag connect, cycle guard, pill condition change, snap, Delete, undo/redo round trip, problem badge + disabled Save, leave guard. Exits 1 on any FAIL or page error. | `wf-ids.txt`. Mutates the Release workflow in the browser but does not save. |\n| `approve-wf.cjs` | Approve inside the card, delay countdown ticks, viewport survives polling, task log opens on click. | `/tmp/semaphore-stand/wf3.txt` with `<workflow id> <run id>` of a run waiting at an approval — seeded by hand, not by `seed-wf.sh`. |\n| `keys-wf.cjs` | Keyboard pan/zoom/fit/reset in the editor and the run view; asserts no effect while typing in the properties panel. | `wf-ids.txt`, run 2 of Release. |\n| `shoot-inputs.cjs` | Shoots the four inputs screenshots above and runs 7 PASS/FAIL checks (checkbox state, mapping rows, hint line, by_name chips with a matched output, output count on a card, Outputs panel in the task dialog). `--dark` for the dark theme; `WF` (default 9, explicit workflow), `WF_BYNAME` (default 1), `OUT` (default `/tmp/semaphore-stand/shots-inputs`). Edges are selected by a dispatched `mousedown` on the path — a real click lands on the condition pill. | `AGENTS/tools/wf-stand/seed-outputs.sh` seeded, a finished run of the current revision of both workflows (`run.sh 9`, `run.sh 1`) for the key hints. |\n| `interact-inputs.cjs` | 10 PASS/FAIL checks of the inputs round trip on workflow `WF` (default 12): select the explicit edge, not dirty before editing, type a key, dirty, explicit icon on the pill, Save → API shows the pair, uncheck → by_name chips, Save → fields gone, original graph restored by PUT. Exits 1 on any FAIL or page error. | Running stand with the seeded project; mutates workflow 12 and restores it. |\n| `shoot-nodes.cjs` | Crops one card per node kind (task hovered with the \"+\", approval, delay, note) with edge pills hidden and zoom 1; source of `docs/static/assets/workflow-node-*.webp`. | `WF` env = id of the \"Docs node kinds\" workflow on the stand (default 4). |\n| `make-hotkeys-svg.py <out.svg>` | Generates the keyboard cheat-sheet `docs/static/assets/workflow-hotkeys.svg`; edit `COLUMNS` and re-run when a shortcut changes. | Python 3 only. |\n</yakaai_skills>"}