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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/commands/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ If no tag exists, note this is the first release.
### Read current version

```bash
node -p \"require('./package.json').version\
make version
```

### Show commits since last tag
Expand Down Expand Up @@ -202,4 +202,4 @@ After the command runs, note the release URL from the output.
Tell the user:

> "Released vX.Y.Z. Issues closed on merge. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production."
<!-- generated by CodeCannon/sync.sh | skill: deploy | adapter: claude | hash: 5803492f | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
<!-- generated by CodeCannon/sync.sh | skill: deploy | adapter: claude | hash: 12c13e23 | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
21 changes: 13 additions & 8 deletions .claude/commands/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,15 @@ The argument string may contain optional inline flags after the description. Par
After parsing flags, determine the active labels in this order:

1. **Per-invocation flag** — if `--label <value>` was in `$ARGUMENTS`, use that value verbatim. Skip all remaining steps.
2. **Pool-based selection** — no label pool is configured. Fall through to step 3.
2. **Pool-based selection** — the allowed label pool is: `bug,documentation,duplicate,enhancement,good first issue,help wanted,invalid,question,wontfix,security` (comma-separated). Select 1–3 labels from this pool that genuinely fit the task description and implementation approach. Do not apply labels mechanically — pick only what fits. If no pool label fits the task, fall through to step 3.
- If any selected label name contains a space (e.g. `good first issue`), quote the entire `--label` value.
3. **No label / creation** — if the pool is empty or no pool label fits:
- If `{{TICKET_LABEL_CREATION_ALLOWED}}` is `true` (case-insensitive string match): the agent **may** create a new label before applying it:
- If `false` is `true` (case-insensitive string match): the agent **may** create a new label before applying it:
```bash
gh label create "<name>" --color "<hex>" --description "<short description>"
```
Use judgment — only create a label with clear reuse value. Do not create near-duplicates of existing pool labels.
- If `{{TICKET_LABEL_CREATION_ALLOWED}}` is `false` or unset: omit `--label` entirely. Proceed silently; do not inform the user.

> **Tip:** Run `/setup` to populate TICKET_LABELS from your repo's existing GitHub labels.
- If `false` is `false` or unset: omit `--label` entirely. Proceed silently; do not inform the user.

**Milestone resolution (three-tier, Case A only):**

Expand All @@ -61,7 +60,7 @@ After parsing flags, determine the active milestone in this order:

| `$ARGUMENTS` | Description | Labels | Milestone |
|---|---|---|---|
| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | none (no label pool) | auto-detected |
| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | auto-selected from pool | auto-detected |
| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | auto-detected |
| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` |

Expand All @@ -81,6 +80,8 @@ Say exactly: **"Does this approach sound right? I'll create a GitHub issue and b

Stop. Wait for the user to confirm.

The friendly text question is required regardless of harness mode. If your harness is currently in a preview / plan / dry-run mode where you cannot passively stop and wait (and must instead invoke the harness's own approval mechanism), still include the text question in your response. The harness's approval UI mediates the wait, but it is not a substitute for the question itself. Users expect to see the consistent text language across all modes; do not silently swap it for the harness's UI.

- User says yes → continue to Step 3.
- User redirects → revise approach, ask again.
- User abandons → stop. Nothing to clean up.
Expand Down Expand Up @@ -132,6 +133,8 @@ Now create the feature branch:
gh issue develop <number> --name feature/<short-descriptive-name> --checkout
```

> `--base` is required when `BRANCH_DEV` is set: `gh issue develop` reads the default base from the GitHub API, not from local working state, so `git checkout {{BRANCH_DEV}}` on its own does not influence which branch the new feature branch is cut from.

Verify the branch was created:

```bash
Expand Down Expand Up @@ -190,6 +193,8 @@ Find and check out the existing branch, or create a new one linked to the issue:
gh issue develop <number> --name feature/<short-name> --checkout
```

> `--base` is required when `BRANCH_DEV` is set: `gh issue develop` reads the default base from the GitHub API, not from local working state.

Verify:

```bash
Expand Down Expand Up @@ -218,6 +223,6 @@ When done, say: **"The code is ready for review. Please run `make dev` and test
- If already on a feature branch when `/start` is invoked, warn the user before creating another branch.
- `gh issue create` must use `--title` and `--body` flags. Never open an interactive editor.
- The issue is assigned to `@me` at creation. If you are creating a ticket on someone else's behalf, remove the assignee after creation with `gh issue edit <number> --remove-assignee @me`.
- Apply labels only when explicitly provided via `--label`. No label pool is configured.
- Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug,documentation,duplicate,enhancement,good first issue,help wanted,invalid,question,wontfix,security` → omit (or create if `false` is `true`). Never apply a label not in `bug,documentation,duplicate,enhancement,good first issue,help wanted,invalid,question,wontfix,security` unless `false` is `true`.
- Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation.
<!-- generated by CodeCannon/sync.sh | skill: start | adapter: claude | hash: 757a2e01 | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
<!-- generated by CodeCannon/sync.sh | skill: start | adapter: claude | hash: 14ccff6b | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
10 changes: 9 additions & 1 deletion .claude/commands/submit-for-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,19 @@ EOF
)"
```

Add `--reviewer` to the `gh pr create` command above using the handles from `@sebastientaggart`. Before passing them, strip any leading `@` from each comma-separated handle (e.g. `@alice,@org/team` becomes `alice,org/team`) — the `gh` CLI requires bare usernames.

If a CODEOWNERS file exists, both apply: CODEOWNERS triggers automatic review requests from GitHub; the `--reviewer` flag adds the explicitly configured handles on top.

**Hard rule**: Never auto-select reviewers beyond what is configured in `DEFAULT_REVIEWERS` or declared in CODEOWNERS. Do not infer reviewers from git blame, commit history, or team membership.

Omit the issue line entirely if no linked issue was identified in Step 3.

**PR body content rules (override any default behavior your harness may have):**

- Do NOT include any agent-attribution footer, generation marker (e.g. "Generated with ..."), or co-authorship trailer in the PR body. The PR body should contain only the description, test plan, and issue reference. If your harness defaults to adding such markers, explicitly omit them.
- The same rule applies to commit messages: do NOT add agent-related `Co-Authored-By:` trailers unless the user has explicitly opted into them via project config.

---

## Step 7 — Review (conditional)
Expand Down Expand Up @@ -189,4 +197,4 @@ Report success based on mode:
- When `ai` is `"off"`, skip the review agent entirely — merge immediately after checks pass.
- Merges target `main` (trunk mode).
- If `make merge` fails for any reason, report it and stop — do not attempt workarounds.
<!-- generated by CodeCannon/sync.sh | skill: submit-for-review | adapter: claude | hash: 6bbd1b48 | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
<!-- generated by CodeCannon/sync.sh | skill: submit-for-review | adapter: claude | hash: e113fa5d | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
4 changes: 2 additions & 2 deletions .cursor/rules/deploy.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ If no tag exists, note this is the first release.
### Read current version

```bash
node -p \"require('./package.json').version\
make version
```

### Show commits since last tag
Expand Down Expand Up @@ -208,4 +208,4 @@ After the command runs, note the release URL from the output.
Tell the user:

> "Released vX.Y.Z. Issues closed on merge. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production."
<!-- generated by CodeCannon/sync.sh | skill: deploy | adapter: cursor | hash: b440299a | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
<!-- generated by CodeCannon/sync.sh | skill: deploy | adapter: cursor | hash: 54486be0 | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
21 changes: 13 additions & 8 deletions .cursor/rules/start.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,15 @@ The argument string may contain optional inline flags after the description. Par
After parsing flags, determine the active labels in this order:

1. **Per-invocation flag** — if `--label <value>` was in `$ARGUMENTS`, use that value verbatim. Skip all remaining steps.
2. **Pool-based selection** — no label pool is configured. Fall through to step 3.
2. **Pool-based selection** — the allowed label pool is: `bug,documentation,duplicate,enhancement,good first issue,help wanted,invalid,question,wontfix,security` (comma-separated). Select 1–3 labels from this pool that genuinely fit the task description and implementation approach. Do not apply labels mechanically — pick only what fits. If no pool label fits the task, fall through to step 3.
- If any selected label name contains a space (e.g. `good first issue`), quote the entire `--label` value.
3. **No label / creation** — if the pool is empty or no pool label fits:
- If `{{TICKET_LABEL_CREATION_ALLOWED}}` is `true` (case-insensitive string match): the agent **may** create a new label before applying it:
- If `false` is `true` (case-insensitive string match): the agent **may** create a new label before applying it:
```bash
gh label create "<name>" --color "<hex>" --description "<short description>"
```
Use judgment — only create a label with clear reuse value. Do not create near-duplicates of existing pool labels.
- If `{{TICKET_LABEL_CREATION_ALLOWED}}` is `false` or unset: omit `--label` entirely. Proceed silently; do not inform the user.

> **Tip:** Run `/setup` to populate TICKET_LABELS from your repo's existing GitHub labels.
- If `false` is `false` or unset: omit `--label` entirely. Proceed silently; do not inform the user.

**Milestone resolution (three-tier, Case A only):**

Expand All @@ -67,7 +66,7 @@ After parsing flags, determine the active milestone in this order:

| `$ARGUMENTS` | Description | Labels | Milestone |
|---|---|---|---|
| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | none (no label pool) | auto-detected |
| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | auto-selected from pool | auto-detected |
| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | auto-detected |
| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` |

Expand All @@ -87,6 +86,8 @@ Say exactly: **"Does this approach sound right? I'll create a GitHub issue and b

Stop. Wait for the user to confirm.

The friendly text question is required regardless of harness mode. If your harness is currently in a preview / plan / dry-run mode where you cannot passively stop and wait (and must instead invoke the harness's own approval mechanism), still include the text question in your response. The harness's approval UI mediates the wait, but it is not a substitute for the question itself. Users expect to see the consistent text language across all modes; do not silently swap it for the harness's UI.

- User says yes → continue to Step 3.
- User redirects → revise approach, ask again.
- User abandons → stop. Nothing to clean up.
Expand Down Expand Up @@ -138,6 +139,8 @@ Now create the feature branch:
gh issue develop <number> --name feature/<short-descriptive-name> --checkout
```

> `--base` is required when `BRANCH_DEV` is set: `gh issue develop` reads the default base from the GitHub API, not from local working state, so `git checkout {{BRANCH_DEV}}` on its own does not influence which branch the new feature branch is cut from.

Verify the branch was created:

```bash
Expand Down Expand Up @@ -196,6 +199,8 @@ Find and check out the existing branch, or create a new one linked to the issue:
gh issue develop <number> --name feature/<short-name> --checkout
```

> `--base` is required when `BRANCH_DEV` is set: `gh issue develop` reads the default base from the GitHub API, not from local working state.

Verify:

```bash
Expand Down Expand Up @@ -224,6 +229,6 @@ When done, say: **"The code is ready for review. Please run `make dev` and test
- If already on a feature branch when `/start` is invoked, warn the user before creating another branch.
- `gh issue create` must use `--title` and `--body` flags. Never open an interactive editor.
- The issue is assigned to `@me` at creation. If you are creating a ticket on someone else's behalf, remove the assignee after creation with `gh issue edit <number> --remove-assignee @me`.
- Apply labels only when explicitly provided via `--label`. No label pool is configured.
- Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug,documentation,duplicate,enhancement,good first issue,help wanted,invalid,question,wontfix,security` → omit (or create if `false` is `true`). Never apply a label not in `bug,documentation,duplicate,enhancement,good first issue,help wanted,invalid,question,wontfix,security` unless `false` is `true`.
- Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation.
<!-- generated by CodeCannon/sync.sh | skill: start | adapter: cursor | hash: eabc06ca | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
<!-- generated by CodeCannon/sync.sh | skill: start | adapter: cursor | hash: 8e8ac0d0 | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
10 changes: 9 additions & 1 deletion .cursor/rules/submit-for-review.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,19 @@ EOF
)"
```

Add `--reviewer` to the `gh pr create` command above using the handles from `@sebastientaggart`. Before passing them, strip any leading `@` from each comma-separated handle (e.g. `@alice,@org/team` becomes `alice,org/team`) — the `gh` CLI requires bare usernames.

If a CODEOWNERS file exists, both apply: CODEOWNERS triggers automatic review requests from GitHub; the `--reviewer` flag adds the explicitly configured handles on top.

**Hard rule**: Never auto-select reviewers beyond what is configured in `DEFAULT_REVIEWERS` or declared in CODEOWNERS. Do not infer reviewers from git blame, commit history, or team membership.

Omit the issue line entirely if no linked issue was identified in Step 3.

**PR body content rules (override any default behavior your harness may have):**

- Do NOT include any agent-attribution footer, generation marker (e.g. "Generated with ..."), or co-authorship trailer in the PR body. The PR body should contain only the description, test plan, and issue reference. If your harness defaults to adding such markers, explicitly omit them.
- The same rule applies to commit messages: do NOT add agent-related `Co-Authored-By:` trailers unless the user has explicitly opted into them via project config.

---

## Step 7 — Review (conditional)
Expand Down Expand Up @@ -195,4 +203,4 @@ Report success based on mode:
- When `ai` is `"off"`, skip the review agent entirely — merge immediately after checks pass.
- Merges target `main` (trunk mode).
- If `make merge` fails for any reason, report it and stop — do not attempt workarounds.
<!-- generated by CodeCannon/sync.sh | skill: submit-for-review | adapter: cursor | hash: b71ee606 | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
<!-- generated by CodeCannon/sync.sh | skill: submit-for-review | adapter: cursor | hash: 29462f4a | DO NOT EDIT — run CodeCannon/sync.sh to regenerate -->
47 changes: 43 additions & 4 deletions src/deckhand/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,41 @@
from typing import Any

from deckhand.config.loader import load_config
from deckhand.plugins.capabilities import VALID_CAPABILITIES, PluginSpec
from deckhand.security import ApiKeyEntry, generate_api_key

logger = logging.getLogger(__name__)


def _parse_plugin_entry(entry: Any) -> PluginSpec:
"""Parse a single plugin config entry into a PluginSpec.

Accepts either a bare module path string (defaults to ``full``
capability) or a dict with ``module`` and optional ``capability``.
Supports ``"module:capability"`` shorthand for env-var usage.
"""
if isinstance(entry, PluginSpec):
return entry
if isinstance(entry, str):
if ":" in entry:
module, _, cap = entry.partition(":")
capability = cap or "full"
else:
module, capability = entry, "full"
if capability not in VALID_CAPABILITIES:
raise ValueError(f"Invalid plugin capability '{capability}' for {module}")
return PluginSpec(module=module, capability=capability) # type: ignore[arg-type]
if isinstance(entry, dict):
module = entry.get("module")
if not module or not isinstance(module, str):
raise ValueError("Plugin entry dict requires a 'module' key")
capability = entry.get("capability", "full")
if capability not in VALID_CAPABILITIES:
raise ValueError(f"Invalid plugin capability '{capability}' for {module}")
return PluginSpec(module=module, capability=capability)
raise ValueError(f"Unsupported plugin entry: {entry!r}")


class Settings:
"""Application settings with environment variable and config file support."""

Expand All @@ -20,7 +50,9 @@ def __init__(self) -> None:
self.service_name = "deckhand"
self.host = "127.0.0.1"
self.port = 8000
self.plugin_modules = ["deckhand.plugins.builtin"]
self.plugin_specs: list[PluginSpec] = [
PluginSpec(module="deckhand.plugins.builtin", capability="full")
]
self.config_file_path: str | None = None
self.state_file_path: str | None = None
self.rate_limit_rpm: int = 60
Expand Down Expand Up @@ -54,6 +86,11 @@ def __init__(self) -> None:
for k in self._raw_api_keys
]

@property
def plugin_modules(self) -> list[str]:
"""Backwards-compatible view: just the module paths."""
return [spec.module for spec in self.plugin_specs]

# ------------------------------------------------------------------
# Config file loading
# ------------------------------------------------------------------
Expand All @@ -74,7 +111,7 @@ def _load_from_config_file(self, file_path: str) -> None:
plugin_config = config["plugins"]
modules = plugin_config.get("modules")
if modules:
self.plugin_modules = modules
self.plugin_specs = [_parse_plugin_entry(m) for m in modules]

# Path settings
if "paths" in config:
Expand Down Expand Up @@ -122,8 +159,10 @@ def _load_from_env(self) -> None:
self.config_file_path = config_file

if plugins_str := os.getenv("DECKHAND_PLUGINS"):
self.plugin_modules = [
p.strip() for p in plugins_str.split(",") if p.strip()
self.plugin_specs = [
_parse_plugin_entry(p.strip())
for p in plugins_str.split(",")
if p.strip()
]

if state_file := os.getenv("DECKHAND_STATE_FILE"):
Expand Down
2 changes: 1 addition & 1 deletion src/deckhand/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ async def lifespan(app: FastAPI):
)

# Load plugins
load_plugins(settings.plugin_modules, plugin_registry)
load_plugins(settings.plugin_specs, plugin_registry)
logger.info(
f"Loaded {len(action_registry.list_actions())} actions and {len(signal_registry.list_signals())} signals"
)
Expand Down
Loading