Skip to content

refactor(hooks): consume normalized skill activations - #5086

Open
danielkov wants to merge 1 commit into
mainfrom
fix/consume-agenthook-skill-activation
Open

refactor(hooks): consume normalized skill activations#5086
danielkov wants to merge 1 commit into
mainfrom
fix/consume-agenthook-skill-activation

Conversation

@danielkov

@danielkov danielkov commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Depends on speakeasy-api/agenthooks#13.

Consume agenthooks.SkillActivationOf as the single projection for tool-based skill activations. The normalized tool output continues through ordinary hook ingestion, and the same event content supplies the hash and payload used by the skill-content registry and offline replay.

Delete Gram’s Claude/Codex/Cursor manifest readers, path classification, symlink authorization, detached file-reopen uploader, and internal upload-skill command. Keep only the separate Codex $skill-name prompt fallback because it has no tool event.


Summary by cubic

Consume normalized skill activations via agenthooks.SkillActivationOf and route tool output through standard hook ingestion. This removes path-based skill detection and the detached upload flow, simplifying skill capture and replay.

  • Refactors

    • Use agenthooks.SkillActivationOf to populate data.skill and emit skill.activated when explicit.
    • Derive skill content from the tool call output; compute rawSHA256 from that. SourcePath and SourceLevel are now nil.
    • Remove Claude/Codex/Cursor manifest readers, path classification, symlink checks, detached uploader, and the upload-skill command.
    • Replay reads content from stored tool output; spool entries no longer store a skill source root.
    • Keep Codex $skill-name prompt fallback.
    • Update tests and internal APIs (e.g., finishExchange, spoolUnsent) to the new flow.
    • Bump github.com/speakeasy-api/agenthooks to a version that provides normalized activations.
  • Migration

    • If you used the upload-skill CLI, remove those calls; skill uploads now use captured tool output directly.
    • Do not rely on SourcePath/SourceLevel in payloads; they are unset with the normalized model. Ensure downstream consumers accept this.

Written for commit 5a2bd00. Summary will update on new commits.

Review in cubic

@danielkov
danielkov requested a review from a team as a code owner August 8, 2026 15:40
@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 5a2bd00

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

atlas migrate lint on server/migrations

Status Step Result
No migration files detected  
ERD and visual diff generated View Visualization
No issues found View Report
Read the full linting report on Atlas Cloud

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

atlas migrate lint on server/clickhouse/migrations

Status Step Result
No migration files detected  
ERD and visual diff generated View Visualization
No issues found View Report
Read the full linting report on Atlas Cloud

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 15 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="hooks/relay/skills.go">

<violation number="1" location="hooks/relay/skills.go:35">
P1: Implicit Codex/Cursor skill completions never reach the registry or uploader: their content is available only on `ToolPost`, but that event has no `payload.Data.Skill` and this resolver exits before consuming the activation. Propagate a skill block/hash for content-bearing normalized completions while retaining `tool.completed`, and cover a post-read delivery.</violation>

<violation number="2" location="hooks/relay/skills.go:88">
P2: `$name` fallback no longer recognizes bundled skills under `/etc/codex/skills/.system` or `/opt/codex/skills/.system`, so those prompt activations are silently omitted. Include each standard root’s `.system` child in this lookup.</violation>
</file>

<file name="hooks/relay/drain.go">

<violation number="1" location="hooks/relay/drain.go:208">
P1: Existing v1 offline skill entries lose their content upload after upgrade: legacy entries have source path/root but no tool output, so this reconstruction returns nil and drain removes them after ingest acceptance. Add a legacy migration/replay path (or version and explicitly preserve legacy entries) before treating all v1 entries as output-backed.</violation>
</file>

<file name="hooks/relay/runner.go">

<violation number="1" location="hooks/relay/runner.go:236">
P2: The live skill-content upload is now synchronous on the relay handler, extending the gating path. Previously this line called `startSkillContentUpload`, which launched a detached child process (`cmd.Start` + `Process.Release`) and returned immediately — the code explicitly stated "content upload never extends the hook's gating path." Now `uploadSkillContent` runs inline against the network with a 30s `skillUploadBudget`, an internal per-attempt retry loop, and SDK backoff up to `retryMaxElapsedMS` (30s). Every tool event that captures a skill therefore blocks the agent's hook round-trip for up to ~30s whenever the upload endpoint is slow or unreachable (the failure is only `debugf`-logged afterward). Consider bounding this delivery well below the gating budget, or deferring it, so a content-upload stall cannot stall the agent's tool loop.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread hooks/relay/skills.go
func captureResolvedSkill(result *resolvedSkill, location skillLocation) *resolvedSkill {
file, authorizedRoot, ok := openValidatedSkill(location)
if !ok {
activation := agenthooks.SkillActivationOf(typed)

@cubic-dev-ai cubic-dev-ai Bot Aug 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Implicit Codex/Cursor skill completions never reach the registry or uploader: their content is available only on ToolPost, but that event has no payload.Data.Skill and this resolver exits before consuming the activation. Propagate a skill block/hash for content-bearing normalized completions while retaining tool.completed, and cover a post-read delivery.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/relay/skills.go, line 35:

<comment>Implicit Codex/Cursor skill completions never reach the registry or uploader: their content is available only on `ToolPost`, but that event has no `payload.Data.Skill` and this resolver exits before consuming the activation. Propagate a skill block/hash for content-bearing normalized completions while retaining `tool.completed`, and cover a post-read delivery.</comment>

<file context>
@@ -18,218 +15,38 @@ import (
-func captureResolvedSkill(result *resolvedSkill, location skillLocation) *resolvedSkill {
-	file, authorizedRoot, ok := openValidatedSkill(location)
-	if !ok {
+	activation := agenthooks.SkillActivationOf(typed)
+	if activation == nil || !activation.ContentAvailable || len(activation.Content) > maxSkillContentBytes || !utf8.ValidString(activation.Content) {
 		return result
</file context>
Fix with cubic

Comment thread hooks/relay/drain.go
}
return &resolvedSkill{
content: "",
content: content,

@cubic-dev-ai cubic-dev-ai Bot Aug 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Existing v1 offline skill entries lose their content upload after upgrade: legacy entries have source path/root but no tool output, so this reconstruction returns nil and drain removes them after ingest acceptance. Add a legacy migration/replay path (or version and explicitly preserve legacy entries) before treating all v1 entries as output-backed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/relay/drain.go, line 208:

<comment>Existing v1 offline skill entries lose their content upload after upgrade: legacy entries have source path/root but no tool output, so this reconstruction returns nil and drain removes them after ingest acceptance. Add a legacy migration/replay path (or version and explicitly preserve legacy entries) before treating all v1 entries as output-backed.</comment>

<file context>
@@ -190,17 +190,25 @@ func drainSpool(ctx context.Context, dir string) DrainSummary {
 	}
 	return &resolvedSkill{
-		content:      "",
+		content:      content,
 		rawSHA256:    *entry.Envelope.Data.Skill.RawSha256,
-		sourcePath:   *entry.Envelope.Data.Skill.SourcePath,
</file context>
Fix with cubic

Comment thread hooks/relay/skills.go
return false
}
home, _ := os.UserHomeDir()
roots := []string{"/etc/codex/skills", "/opt/codex/skills"}

@cubic-dev-ai cubic-dev-ai Bot Aug 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: $name fallback no longer recognizes bundled skills under /etc/codex/skills/.system or /opt/codex/skills/.system, so those prompt activations are silently omitted. Include each standard root’s .system child in this lookup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/relay/skills.go, line 88:

<comment>`$name` fallback no longer recognizes bundled skills under `/etc/codex/skills/.system` or `/opt/codex/skills/.system`, so those prompt activations are silently omitted. Include each standard root’s `.system` child in this lookup.</comment>

<file context>
@@ -249,327 +66,52 @@ func codexPromptSkill(prompt, cwd string) (string, skillLocation) {
+		return false
 	}
 	home, _ := os.UserHomeDir()
+	roots := []string{"/etc/codex/skills", "/opt/codex/skills"}
 	if home != "" {
-		root := filepath.Join(home, ".agents", "skills")
</file context>
Suggested change
roots := []string{"/etc/codex/skills", "/opt/codex/skills"}
roots := []string{
"/etc/codex/skills", filepath.Join("/etc/codex/skills", ".system"),
"/opt/codex/skills", filepath.Join("/opt/codex/skills", ".system"),
}
Fix with cubic

Comment thread hooks/relay/runner.go
commitPromptAttachmentHighWater(promptAttachmentAdvance)
}
if err := startSkillContentUpload(finalCreds, res, resolvedSkill); err != nil {
if err := uploadSkillContent(ctx, finalCreds, res, resolvedSkill); err != nil {

@cubic-dev-ai cubic-dev-ai Bot Aug 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The live skill-content upload is now synchronous on the relay handler, extending the gating path. Previously this line called startSkillContentUpload, which launched a detached child process (cmd.Start + Process.Release) and returned immediately — the code explicitly stated "content upload never extends the hook's gating path." Now uploadSkillContent runs inline against the network with a 30s skillUploadBudget, an internal per-attempt retry loop, and SDK backoff up to retryMaxElapsedMS (30s). Every tool event that captures a skill therefore blocks the agent's hook round-trip for up to ~30s whenever the upload endpoint is slow or unreachable (the failure is only debugf-logged afterward). Consider bounding this delivery well below the gating budget, or deferring it, so a content-upload stall cannot stall the agent's tool loop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/relay/runner.go, line 236:

<comment>The live skill-content upload is now synchronous on the relay handler, extending the gating path. Previously this line called `startSkillContentUpload`, which launched a detached child process (`cmd.Start` + `Process.Release`) and returned immediately — the code explicitly stated "content upload never extends the hook's gating path." Now `uploadSkillContent` runs inline against the network with a 30s `skillUploadBudget`, an internal per-attempt retry loop, and SDK backoff up to `retryMaxElapsedMS` (30s). Every tool event that captures a skill therefore blocks the agent's hook round-trip for up to ~30s whenever the upload endpoint is slow or unreachable (the failure is only `debugf`-logged afterward). Consider bounding this delivery well below the gating budget, or deferring it, so a content-upload stall cannot stall the agent's tool loop.</comment>

<file context>
@@ -235,11 +229,11 @@ func (r *Relay) deliver(ctx context.Context, typed any) (ingestResult, authState
 		commitPromptAttachmentHighWater(promptAttachmentAdvance)
 	}
-	if err := startSkillContentUpload(finalCreds, res, resolvedSkill); err != nil {
+	if err := uploadSkillContent(ctx, finalCreds, res, resolvedSkill); err != nil {
 		r.debugf("skill upload: %v", err)
 	}
</file context>
Fix with cubic

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant