Skip to content

feat(cli): support a single-clone global install shared by several CLIs #859

Closed

Description

@dkp-consult

Problem

The framework installs per project, through a marketplace and the host's plugin
system. There is no supported path for a machine-wide install shared by several
CLIs from a single clone
— which is what a solo developer running four agentic
CLIs side by side actually needs.

Working around it surfaced three concrete gaps, all on v5.10.0 with
@ai-driven-dev/cli@5.3.0:

1. translate --as flat emits hook files but never their declaration.
plugins/aidd-telemetry/hooks/hooks.json declares the three events the journal
needs (SessionStart, Stop, PostToolUse, lines 8/18/28, keyed on
${CLAUDE_PLUGIN_ROOT}). Neither --to claude --as flat nor --to codex --as flat
reproduces it: both write hooks/aidd-telemetry/journal.cjs and its lib/, and no
hooks.json anywhere in the output tree. A hook that is placed but never declared
does not fail — it simply never runs, silently. Outside the plugin runtime that
substitutes ${CLAUDE_PLUGIN_ROOT}, the declaration has to be hand-written into
each host's own config format.

2. Telemetry silently records nothing on two shipping CLIs.
DECLARED_HOSTS (plugins/aidd-telemetry/hooks/lib/host.cjs:25) covers
claude-code, codex, copilot, cursor, opencode. Kimi Code and Antigravity
(agy) are absent, so detectHost returns null and
handleUnrecognisedPayload (lib/record.cjs:255) writes a single
_unrecognised.jsonl marker line instead of a run file. Both CLIs load and run
AIDD skills perfectly well — only the measurement layer cannot read them.

3. framework build was removed without a deprecation path.
CLI 5.x split it: lifecycle stayed on framework, conversion moved to
translate, and the options were renamed (--source → positional, --target →
--to, --flat → --as flat). An install script written against 5.x-minus-one
dies on error: unknown command 'build' with no pointer to the replacement.

Scope

  • Document (or support) the single-clone, multi-CLI global install as a first-class
    layout, distinct from the per-project marketplace install.
  • Make translate --as flat emit each target's hook declaration alongside the hook
    files, with absolute paths substituted for ${CLAUDE_PLUGIN_ROOT}.
  • Add Kimi Code and Antigravity to DECLARED_HOSTS and to detectHost, with the
    payload shape each one sends.
  • Publish the CLI availability matrix below in the docs, so an integrator knows what
    is portable before writing a deployment script.

Acceptance criteria

  • aidd translate <src> --to claude --as flat --out <dir> writes a hook
    declaration for every hook it installs, with no ${CLAUDE_PLUGIN_ROOT} left
    unresolved.
  • Same for --to codex --as flat.
  • A session run in Kimi Code produces a run file under aidd_docs/runs/, not a
    _unrecognised.jsonl marker, when the project has opted in.
  • Same for Antigravity.
  • aidd framework build exits with a message naming translate and the renamed
    options, rather than unknown command.
  • The docs carry a per-surface, per-CLI support table.

Prior art in this repo

  • plugins/aidd-telemetry/hooks/hooks.json — the declaration that exists upstream
    and is dropped by the flat layout.
  • plugins/aidd-telemetry/hooks/lib/host.cjs:25 — DECLARED_HOSTS.
  • plugins/aidd-telemetry/hooks/lib/record.cjs:250-269 — the unrecognised-payload
    path, which is exactly why the gap is silent rather than loud.
  • plugins/aidd-telemetry/hooks/lib/repo.cjs:141 — telemetryEnabled, the strict
    === true opt-in gate. Worth noting: the global install stays inert by default
    because of this, which is what makes a machine-wide hook acceptable at all.
  • plugins/aidd-context/hooks/update_memory.js — host-agnostic by construction: it
    rewrites the open project's CLAUDE.md / AGENTS.md and never reads who called
    it. This is why it works on all four CLIs while the journal works on two.

Out of scope

  • Agent deployment to Antigravity. agy exposes --agent and an agents
    subcommand, but agy agents returns nothing and file-based profiles dropped in
    ~/.gemini/config/agents, ~/.gemini/agents and ~/.gemini/config/subagents are
    ignored. Its subagents look internal rather than file-declared — nothing to
    deploy until that changes.
  • Turning measurement on. That stays per project via
    .aidd/config.json, as today.

How the global install works

A ~250-line shell script, idempotent, re-runnable. The design decisions are the part
worth discussing — several of them are constraints a framework install --global
would hit too.

One clone is the source of truth, and it is not vendored. The framework lives at
~/dev/aidd-framework; my repo versions how to deploy it, never its content. A
--pull flag fast-forwards the clone, and the version reported is the latest v*
tag — not git describe, which picks up the per-plugin tags (aidd-context-v2.7.0)
first, and not the root package.json, still at 0.0.0.

Two builds, four destinations. translate --to claude --as flat provides the
skills — identical for all four CLIs — plus the Markdown agents and the hooks.
--to codex --as flat is built only to harvest the TOML agents. Everything is
staged in a temp directory first, so a failed build never touches a live config;
when framework build disappeared in CLI 5.x, set -e stopped the run before a
single file moved.

Four skill roots, one format. ~/.claude/skills, ~/.codex/skills,
~/.kimi-code/skills, ~/.gemini/config/skills. All four load the same
<name>/SKILL.md bundle with no translation — this is the single most portable
thing in the framework.

Every write is prefix-scoped to aidd-. Nothing outside that prefix is written
or removed, so a cleanup pass can never eat a hand-written skill living in the same
directory. A bundle that disappears upstream is removed locally: without that, a
renamed skill lingers as a duplicate and keeps being offered to the model. Bundle
directories are replaced wholesale rather than merged — the telemetry hook loads
hooks/lib/, and a module dropped upstream must not survive locally as dead code
the entry point still requires.

The script never writes a CLI's configuration file. ~/.claude/settings.json,
~/.codex/hooks.json, ~/.gemini/settings.json and ~/.kimi-code/config.toml
already carry hooks that are not mine. Instead it verifies two things per file — a
declaration exists, and the path inside it resolves to a real file — and prints the
block to paste when either fails. The second check is the one that matters: a
declaration left pointing at an old path fails silently forever.

One wrinkle worth flagging for anyone automating this: JSON allows \/ for a
forward slash, and one of these files is written in that dialect. Searching for the
hook path verbatim reports a perfectly valid declaration as missing.

A separate consistency checker, at two levels. Disk-only is instant and free: it
fingerprints the aidd- bundles across the four roots and compares them, so a
partial deployment shows up as a fingerprint mismatch. --live adds one real model
call per CLI — the probe opens a skill bundle and asks for the filenames in its
actions/ directory, which fails when the bundle is present but stripped of its
companion files. The two levels do not prove the same thing, and the first does not
replace the second: a CLI that has not been restarted after an update shows a
perfect disk and still sees nothing.

Where the hooks live. ~/.aidd/hooks/, a neutral root — not under ~/.claude/,
which would put Codex's configuration inside another tool's directory. The memory
hook goes to all four CLIs because it never reads who called it; the telemetry
journal goes to two, for the host-detection reason above.

What I measured

One clone of the framework, built twice with translate (--to claude for skills,
Markdown agents and hooks; --to codex for TOML agents), then deployed to four CLIs.
50 skills, 2 agents. Each row below was checked by running the CLI, not by reading
its docs — a probe opens a skill bundle and lists its actions/ directory, so it
fails when the bundle is present but stripped of its companion files.

Surface Claude Code Codex Kimi Code Antigravity
Skills (<name>/SKILL.md) yes yes yes yes
Agents yes (.md) yes (.toml) yes (.md, Claude format verbatim) no target format
Memory hook (update_memory.js) yes yes yes yes
Telemetry hook (journal.cjs) yes yes no — host unknown no — host unknown

The skill bundle format is the portable one: all four load the same
<name>/SKILL.md tree with no translation. Kimi reads the Claude agent Markdown
as-is. Hook declaration is where the four diverge — Claude Code and Codex share a
JSON shape, Antigravity uses a similar JSON under ~/.gemini/settings.json, and
Kimi uses flat TOML [[hooks]] entries.

Happy to open this as separate scoped issues if you would rather validate them one
at a time, and to take the telemetry host-detection one myself.

Activity

  1. blafourcade commented on Sep 14, 2026

    @blafourcade
    Contributor

    This issue mixes work that is already complete with two new host integrations.

    Already resolved on current next:

    The remaining question is Kimi Code and Antigravity compatibility.

    Keep this issue as a Spike to verify, for each host:

    • the supported global location and configuration ownership;
    • which framework artifacts can be installed natively;
    • the hook declaration and payload contract needed by aidd-telemetry;
    • one captured, redacted fixture proving the telemetry path, or a documented reason it cannot be supported.

    Kimi Code uses user skills under ~/.kimi-code/skills/ and hook declarations in ~/.kimi-code/config.toml. Antigravity’s global integration is a plugin under ~/.gemini/config/plugins/, which can carry skills, rules, MCP and hooks. They therefore cannot share the existing Claude/Codex configuration logic unchanged.

    Out of scope: a framework installer rewriting global configuration for all tools, a framework build alias, and further Claude/Codex flat-hook work.

    The spike should end with a small compatibility table and separately scoped implementation tasks only where the evidence proves support.

  2. moved this from Ideation to Todo in AIDD Roadmapon Sep 14, 2026
  3. dkp-consult commented on Sep 17, 2026

    @dkp-consult
    ContributorAuthor

    TL;DR — both hosts are supportable. Kimi Code needs a tool profile plus a journal host;
    Antigravity needs a journal host and turns out to have three install targets, one of them
    machine-wide. Patches exist for both. Two unrelated defects found on the way, kept out of this
    thread.

    Spike done. Everything below was measured on a local checkout of today's next, not read in a
    doc — agy 1.2.5, Kimi Code 0.43.1, CLI 5.3.0 built from source. Most claims carry the command
    that reproduces them, so you can check the ones that matter to you rather than take my word.

    Version note that matters for the decision: agy updated itself three times during this spike
    (1.2.1 → 1.2.5) and Kimi moved 0.40.1 → 0.43.1. Anything pinned to those binaries is perishable
    by construction.

    Compatibility table

    Surface Claude Code Codex Kimi Code Antigravity (agy)
    Skills (<name>/SKILL.md) yes yes ~/.kimi-code/skills/ and .agents/skills/, user + project ~/.gemini/config/skills/ and <project>/.agents/skills/
    Agents .md .toml Claude .md verbatim — .kimi-code/agents/, .agents/agents/, both scopes <name>/agent.md, a directory like a skill — <project>/.agents/agents/ and ~/.gemini/config/agents/. Not a flat .md.
    Rules CLAUDE.md AGENTS.md AGENTS.md rules/*.md in any customization root, GEMINI.md, AGENTS.md
    Hook declaration JSON settings.json JSON hooks.json TOML [[hooks]] in config.toml JSON — a file named hooks.json, and only that
    Project-scope hooks yes yes no — global only yes, <project>/.agents/hooks.json, gated by folder trust
    Machine-wide hook target not measured here not measured here config.toml (its only scope) ~/.gemini/config/hooks.json, no plugin packaging
    Telemetry hook (journal.cjs) yes yes workable — contract confirmed live on 0.43.1 workable — proven end-to-end, both scopes

    Kimi's roots are named in its own bundle (PROJECT_GENERIC = [".agents/skills"],
    USER_GENERIC = [".agents/agents"], …) and its importer says it out loud: "Do not migrate
    .agents content. Kimi Code already supports .agents skills and AGENTS files by default."

    Antigravity's are in its embedded help: "Workspace Customizations Root … Path: .agents", and
    "Within any of the customization roots above, you can define: Skills, Rules, Plugins, MCP
    Servers (mcp_config.json) and Hooks (hooks.json)."

    The symptom, reproduced on today's next

    In a fresh opted-in repo, unpatched journal: a Kimi SessionStart and an Antigravity
    SessionStart both produce aidd_docs/runs/_unrecognised.jsonl and nothing else. detectHost
    misses every payload from both. Run from a cwd outside any repository, the journal writes
    nothing at all — not even the marker — and exits 0.

    Antigravity: the gate is folder trust

    My first comment said agy had no project hook scope and no way around an empty
    workspacePaths. Both wrong: I had tested four paths under .gemini/ and .agy/, and the
    customization root is .agents/. What decides whether a project hook runs is whether the
    folder is trusted
    , not whether the session is interactive — and agy writes it in its own log
    at every start (~/.gemini/antigravity-cli/log/cli-*.log):

    I0917 08:39:31.103307   1 hooks_manager.go:53] loaded 1 named hooks from 1 hooks.json file(s)
    I0917 08:39:31.114683 327 hooks_manager.go:53] loaded 6 named hooks from 2 hooks.json file(s)
    

    Eleven milliseconds apart: that is the answer to "Yes, I trust this folder". Its own changelog
    confirms the mechanism — "Fixed workspace-local hooks defined in <workspace>/.agents/hooks.json
    not loading after trusting a folder by reloading hooks whenever workspaces change."

    --add-dir <repo> opens the same door headless with no trust granted at all. (named hooks
    counts top-level groups, not events.)

    Three install targets, not two. <project>/.agents/hooks.json, a global plugin, and
    ~/.gemini/config/hooks.json — the global customization root, which I had flagged last round as
    untested by anyone, mine included. It is tested now: a journal wired only there, with the
    project hook disabled, writes a correct run file inside the repository
    (tool:"antigravity", session_start → step_start on a skill → turn_end), because the host
    resolves the repo from workspacePaths[0] rather than from its own cwd (~/.gemini/config).
    That is a native machine-wide target needing no plugin packaging. I removed the file afterwards;
    the log going back to 1 named hooks from 1 file is the closing witness.

    Three findings that outlive my conclusions

    1. SessionStart is a hook event, and it is undocumented. The embedded guide lists five
    events; the embedded proto carries six (exa.hooks_pb.SessionStartHookArgs). Declared in a
    hooks.json it fires with the full payload and agy plugin validate accepts it. So the journal
    maps 1:1 — SessionStart → session-start, Stop → turn-end, PostToolUse → tool-used —
    with no need to repurpose PreInvocation, which 1.2.5 reports as "deprecated and has no
    effect"
    anyway.

    2. agy says which surface it loaded, for free. One grep in cli-*.log, or
    agy -p "/hooks", which answers in print mode without starting a turn or spending quota. Two of
    my passes built probe plugins to obtain a fact the binary writes down on its own.

    3. A defect in aidd-telemetry itself, fixed in task C. handleFileWritten resolved the
    repository from payload.cwd instead of the host's readCwd. Any host whose payload carries no
    cwd — Antigravity names only workspacePaths — had every stated write silently degraded to
    the observed pass. Same payload, same repo, hook cwd outside it: before the fix the run file
    holds session_start alone, after it holds session_start + file_written
    (source: "tool-stated"). No declared host changes behaviour — the four with writtenPath: null
    (Codex, Copilot, Cursor, OpenCode) never reach that line, and Claude Code and Kimi declare
    readCwd: (payload) => payload.cwd, so the value is identical for them.

    Kimi Code

    I have a working patch for this, if it is useful to you. On a clean checkout of today's next
    it applies as-is and takes the journal suite from 197 to 201 and the thirteen telemetry
    suites from 310 to 314, with five fixtures and four tests of its own. It is yours to take,
    adapt or ignore — I mention it only so the work is not done twice.

    The payload contract is confirmed on 0.43.1, from a fresh capture, not from the fixtures. I
    re-authenticated, declared a probe hook, ran two real sessions, and removed the declaration
    afterwards (the file is byte-identical to its backup). What came back matches the fixtures field
    for field: client_type: "kimi_code_cli", session_id and cwd in Claude's spelling, no
    transcript_path, a write as tool_name: "Write" with tool_input.path relative to cwd,
    and a skill call as tool_name: "Skill" with tool_input.skill. Replayed through the patched
    journal in a fresh opted-in repository, those live payloads produce session_start →
    file_written (source: "tool-stated" — the relative-path resolution holds) → turn_end, and
    step_start naming the skill, all with "tool":"kimi-code".

    One packaging detail found on the way, relevant to D: Kimi requires name in a directory
    skill's frontmatter and silently ignores the skill without it, where agy falls back to the
    directory name. AIDD's own skills all carry name, so nothing is broken today — but a shared
    .agents/ tree has to satisfy the stricter host.

    One consequence to decide up front: Kimi has no project hook scope (kimi doctor from inside a
    repo lists only the global config.toml; its importer refuses to migrate hooks), so the
    declaration must be global, merged idempotently into a config.toml that is not ours — on this
    machine it already carries fifteen [[hooks]] entries from another tool. A global hook stays
    inert until a repo opts in (telemetry.enabled === true, strict, plus an AIDD_TELEMETRY=0
    personal refusal). A cost to assume, not a blocker.

    What I withdraw

    "No project-level hook scope in agy" — wrong namespace. "workspacePaths is empty on every
    event"
    — true only of -p without --add-dir. "Nobody has tested ~/.gemini/config/hooks.json"
    — no longer true. My GEMINI_API_KEY note — the variable comes from this machine's shell, I
    never ran the control. "No target format for agents" — a Claude .md passes plugin validate.
    And you were right about rules: no rules component in a plugin, but rules/*.md, GEMINI.md
    and AGENTS.md are read from any customization root. Point 3 (framework build): withdrawn.

    Point 1 is verified fixed on today's next: --to claude --as flat and --to codex --as flat
    both emit a resolved hook declaration, with zero unresolved ${CLAUDE_PLUGIN_ROOT} across 467
    and 468 written files.

    Tasks

    Scope State
    A Kimi Code tool profile in the CLI registry: ~/.kimi-code/, skills, Claude-format agents, a TOML [[hooks]] writer, telemetryJournalHost: "kimi-code". Prerequisite to B. to open
    B kimi-code journal host. patch available — 201/201 journal, 314/314 telemetry
    C antigravity journal host, as a delta of B: detectHost on conversationId + transcriptPath, repo from workspacePaths[0], 1:1 event mapping, write_to_file/TargetFile extractor, skill step read off the SKILL.md view, plus the handleFileWritten fix above. Documents the trust step. patch available — 207/207 journal, 320/320 telemetry, proven over the wire
    D .agents/ as a portable target: Kimi and Antigravity both read it, and translate --to codex --as flat already writes that tree today — 51 skills, and agents would be the two in aidd-dev. Two hosts, no new profile. Independent of A. to open

    On agents specifically. Two files, but a silent failure mode, which is the only reason it
    gets a paragraph. Both hosts read <project>/.agents/agents/, and not in the same shapes: Kimi
    accepts a flat <name>.md and <name>/agent.md, Antigravity accepts only
    <name>/agent.md — a directory, like a skill — at <project>/.agents/agents/ or
    ~/.gemini/config/agents/. The directory shape is therefore the intersection, and it is
    measured on both: written once, kimi --agent <name> and agy --add-dir <repo> --agent <name>
    each execute the agent's own prompt, headless, with no folder trust needed.

    The flat shape fails quietly in three different ways, which is what makes it worth a warning.
    agy never surfaces it. agy --agent <name> then falls back to the default agent without a
    word, and an invented name behaves identically, so the flag confirms nothing either way — where
    Kimi refuses an unknown name and lists the profiles it found. And agy plugin validate reports
    agents : 1 processed for a flat .md the loader will never load, so a green validation is not
    evidence. Today translate --to codex --as flat writes .agents/skills/ only, with agents going
    to .codex/agents/*.toml, so D would be adding that tree — the shape it should take is settled.

    Happy to split, drop or reorder any of this — you know the roadmap, I only know what I measured.
    If B and C are useful as PRs I can open them, one at a time rather than bundled; if you would
    rather implement them yourself, the findings above are all you need and the patches are just a
    shortcut.

    Two things I kept out of this thread

    Both measured, neither about Kimi/Antigravity telemetry:

    1. AIDD's memory hook is inert under Antigravity and fails silently — declared in
      ~/.gemini/settings.json in the Gemini-CLI vocabulary (BeforeAgent, AfterAgent,
      BeforeTool, AfterTool), four strings with zero occurrences in the 1.2.5 binary, whose
      hook manager loads hooks.json files only. A hook in a file the tool never reads is
      indistinguishable from a hook that ran and did nothing.
    2. aidd setup --scope user --plugins … throws an uncaught UserScopePluginModeError with a
      raw stack trace, where plugin install --scope user refuses the same thing cleanly
      ("Tool 'claude' does not support scope 'user'"). The message is good, it is just thrown
      instead of rendered. A framework installer rewriting global configuration is out of scope by
      your own ruling — this is error hardening plus documenting the real installScope: "user"
      matrix, since the first half of a user-scope install already works.

    Want me to open these as two separate issues? Both are written up with their reproduction
    commands — say the word and I file them, or I fold them in here if you would rather keep one
    thread.

  4. blafourcade commented on Sep 17, 2026

    @blafourcade
    Contributor

    @dkp-consult Yes to both. Open the PRs, one at a time as you proposed, and open the two unrelated defects as their own issues.

    Thank you for the withdrawals as much as the findings — a spike that names what it got wrong last round is worth more than one that only reports wins.

    Order, and one change to it.

    Split the handleFileWritten fix out of task C and send it first, on its own.

    It is a defect in shipped code, not part of a new host: handleFileWritten resolving the repository from payload.cwd instead of the host's readCwd degrades every stated write to an observed one for any host whose payload carries no cwd. That it currently harms no declared host is what makes it worth landing alone — it reviews in minutes, it needs no roadmap decision, and burying it inside an Antigravity PR means it waits on a host we have not scheduled. Your before/after (session_start alone, versus session_start + file_written with source: "tool-stated") is the regression test.

    Then:

    • C (antigravity journal host) next. It is autonomous, you have proven it over the wire in both scopes, and it does not wait on A.
    • A (Kimi Code tool profile) before B, as you said. A is the one that needs a roadmap decision rather than a patch: a TOML [[hooks]] writer and a global-only hook scope are both new shapes for the CLI registry, and merging idempotently into a config.toml we do not own is a commitment, not a detail. Open it as an issue with the surfaces you measured and I will rule on it there, before you spend more on B.
    • D (.agents/ as a portable target) independently, whenever. Two hosts reading one tree we already write is the cheapest win in your table.

    On the agents shape, your directory-intersection finding settles it and should be written down where it will be read, not only in this thread: <name>/agent.md is the only shape both hosts load, and the three silent failures of the flat shape — agy never surfacing it, --agent <name> falling back without a word, plugin validate reporting agents : 1 processed for a file the loader ignores — belong in D's issue body. A green validation that is not evidence is exactly the kind of claim this project has been burned by.

    Two constraints to carry into the PRs, neither a blocker:

    • Version perishability is your point and I take it. Nothing in a profile should assert a binary's version; agy moving 1.2.1 → 1.2.5 mid-spike is the argument. Where a capability depends on an undocumented event (SessionStart in the proto but not the guide), the code detects rather than assumes, and the reference cell carries the date you read it — that is the rule fix(aidd-context): source-backed tool references, with dates and unverified cells #618 exists to establish.
    • Folder trust for Antigravity needs to be documented where a person hits it, not only in C's commit message. A hook that stays inert until a folder is trusted is indistinguishable from a hook that ran and did nothing, which is the same failure mode as your out-of-thread finding 1.

    Finding 2 — that agy writes which surface it loaded into cli-*.log, and answers /hooks in print mode without spending quota — is the most reusable thing in the report. Put it in C's issue or reference docs so the next person does not build a probe plugin for a fact the binary volunteers.

    One question back, so B and C do not end up blocked on review: does your .agents/ work assume the machine-wide ~/.gemini/config/hooks.json target, or is it independent of it? You proved that target works and resolves the repo from workspacePaths[0], but a machine-wide install rewriting global configuration is the thing this issue originally asked for and that I have not ruled on. If D stands without it, keep them separate.


    Generated by Claude Code

  5. blafourcade commented on Sep 24, 2026

    @blafourcade
    Contributor

    Spike conclusion

    The compatibility question is resolved. Kimi Code and Antigravity are supportable, but a single generic global installer is not the owning abstraction: portable artifacts can be shared, while hook declarations and configuration ownership remain host-specific.

    Follow-up ownership:

    The original flat-hook and retired framework build concerns were withdrawn or already fixed on next. The spike's evidence and remaining delivery are now durably owned, so #859 can close as completed without waiting for the successor Features and Bugs to ship.

  6. closed this as completedby moving to Done in AIDD Roadmapon Sep 24, 2026
  7. moved this from Todo to Done in AIDD Roadmapon Sep 24, 2026
  8. moved this from Done to In review in AIDD Roadmapon Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    Medium

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions