Skip to content

Copy skills from aw.yml manifest first; copy skill folders recursively and safely#35946

Merged
pelikhan merged 6 commits into
mainfrom
copilot/update-skill-copy-logic
May 30, 2026
Merged

Copy skills from aw.yml manifest first; copy skill folders recursively and safely#35946
pelikhan merged 6 commits into
mainfrom
copilot/update-skill-copy-logic

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 30, 2026

Skill resolution had two gaps: manifest-listed skills weren't guaranteed to appear before auto-scanned ones, and only top-level files in a skill folder were copied (subdirectories were silently dropped). This PR also hardens the new behavior based on review feedback.

Behaviour changes

  • Manifest-first ordering — skills listed under skills: in aw.yml are resolved first; skills/ is then auto-scanned and any additional skill folders not already covered by the manifest are appended.
  • Recursive folder copy — the entire skill folder is copied, including all files at any nesting depth (e.g. scripts/, prompts/, scripts/helpers/).
  • Best-effort auto-scan with explicit manifest skills — if manifest skills are present, transient auto-scan failures no longer fail package resolution; a warning is emitted and manifest skills are still installed.
  • Safer skill file writes — skill file destination paths are validated so files cannot escape the target skill directory.

Implementation

  • pkg/parser/remote_fetch.go — adds listDirAllFilesRecursivelyForHost (Contents API walk) and a git-based fallback listDirAllFilesRecursivelyViaGitForHost (ls-tree -r), plus a recursion depth guard for recursive directory traversal.
  • pkg/cli/add_package_manifest.go — new injectable listPackageDirFilesRecursivelyForHost var; resolvePackageSkillFiles rewritten with manifest-first ordering, full-path deduplication, recursive listing per skill dir, and non-fatal auto-scan errors when manifest skills are explicit.
  • pkg/cli/add_command.goaddSkillFileWithTracking now derives relative paths using skill-root path components and validates the final destination remains within the skill directory (prevents traversal/flattening issues while preserving nested structure).
  • Tests — expanded coverage in:
    • pkg/cli/add_package_manifest_test.go for manifest-first + auto-scan extras, nested skill files, and auto-scan error warning behavior.
    • pkg/cli/add_command_test.go for correct nested path reconstruction, traversal rejection, and invalid path-shape rejection.
    • pkg/parser/remote_fetch_test.go for recursion depth guard behavior.

Copilot AI and others added 2 commits May 30, 2026 15:19
…ursively

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…lPath extraction

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI requested a review from pelikhan May 30, 2026 15:22
@pelikhan pelikhan marked this pull request as ready for review May 30, 2026 15:24
Copilot AI review requested due to automatic review settings May 30, 2026 15:24
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes two gaps in package skill resolution: ensures manifest-listed skills are resolved before auto-scanned ones, and copies skill folders recursively (including nested subdirectories) instead of only top-level files.

Changes:

  • Add manifest-first ordering with auto-scan appended for any skills not already covered (deduplicated by skill name).
  • Add recursive directory listing helpers (ListDirAllFilesRecursivelyForHost + git fallback) and use them when listing skill folder contents.
  • Preserve nested file structure on disk by reconstructing each file's relative path under its skill name when installing.
Show a summary per file
File Description
pkg/parser/remote_fetch.go Adds recursive Contents-API and git ls-tree -r listing helpers.
pkg/cli/add_package_manifest.go Reorders skill resolution (manifest-first + always-auto-scan + dedup) and switches to recursive listing per skill dir.
pkg/cli/add_command.go Reconstructs the relative path under the skill dir using a /<skill>/ boundary so nested files are written under the matching subdirectory.
pkg/cli/add_package_manifest_test.go Wires the new injectable recursive lister into existing tests and adds two cases (manifest-first ordering, nested files).

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 0

…recursive copy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (322 new lines under pkg/) but did not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/35946-manifest-first-skill-ordering-and-recursive-copy.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff. It captures two decisions: (a) manifest-listed skills resolve first and auto-scan always appends not-yet-seen extras, and (b) skill folders are copied recursively, preserving nested subdirectories.
  2. Complete the missing sections — confirm the rationale, refine the alternatives, and (importantly) reconcile the relaxed nesting contract with ADR-35778, whose normative section asserts "single-level nesting / direct child only". Consider marking the relevant clauses of ADR-35778 as superseded.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-35946: Manifest-First Skill Ordering and Recursive Skill Folder Copy

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 35946-manifest-first-skill-ordering-and-recursive-copy.md for PR #35946).

🔒 This PR is blocked from merging until an ADR is linked in the PR body.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · opus48 918.4K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnose, /tdd, and /zoom-out. The PR fixes two real silent-data-loss bugs cleanly and ships good regression tests. Four observations worth addressing — none are blocking on their own, but together they'd harden a correctness-focused PR.

📋 Key Themes & Highlights

Key Themes

  • Unbounded recursion in listContentsRecursively — no depth cap, GitHub Contents API silently truncates at 1000 entries per directory
  • Misleading guard in the git fallback — afterDirPath != "" doesn't actually filter non-matching lines
  • Silent flattening fallback in add_command.go — when strings.Index returns -1, filepath.Base quietly loses the nested path, reintroducing the bug this PR set out to fix
  • Deduplication key in appendIfNew uses base name rather than full path — two paths with the same base name silently collapse

Positive Highlights

  • ✅ The strings.Index with /skillName/ boundary guards prevent false truncation when skill name appears in a parent segment — nice
  • ✅ New tests directly cover both fixed bugs (ordering and nested files) with clear arrange/act/assert structure
  • manifestSkillDirSet cleanly separates the SKILL.md validation concern from dedup logic
  • ✅ Auth-error fallback pattern in the new recursive function is consistent with existing listDirAllFilesForHost

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1.9M

func listDirAllFilesRecursivelyViaGitForHost(owner, repo, ref, dirPath, host string) ([]string, error) {
remoteLog.Printf("Git fallback for listing all dir files recursively: %s/%s@%s (path: %s)", owner, repo, ref, dirPath)

tmpDir, err := getOrCreateListRepoClone(owner, repo, ref, host)
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.

[/diagnose] listContentsRecursively has no depth limit — a repo with a deep or cyclic directory tree (e.g. symlinks on GitHub) could trigger unbounded recursion and exhaust the stack or make many unexpected API calls.

💡 Suggestion

Add a depth parameter and bail out after a reasonable limit (e.g. 10 levels):

const maxSkillDirDepth = 10

func listContentsRecursively(client *api.RESTClient, owner, repo, ref, dirPath string, depth int) ([]string, error) {
    if depth > maxSkillDirDepth {
        return nil, fmt.Errorf("skill directory %q exceeds max depth (%d)", dirPath, maxSkillDirDepth)
    }
    // ...
    subFiles, err := listContentsRecursively(client, owner, repo, ref, item.Path, depth+1)
}

Also worth documenting: the GitHub Contents API caps at 1000 entries per directory request. Files are silently truncated for oversized directories. A comment noting this known limit would help future maintainers.

}
}

remoteLog.Printf("Found %d files recursively in dir via git for %s/%s@%s (path: %s)", len(files), owner, repo, ref, dirPath)
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.

[/diagnose] The afterDirPath != "" guard doesn't actually filter non-matching lines — strings.TrimPrefix returns the original string unchanged when the prefix isn't present, so any non-empty line passes the check. The git ls-tree command already scopes output to files under cleanDirPath/, so in practice this is harmless, but the comment "Include all files at any depth under dirPath" and the conditional together are misleading.

💡 Suggestion

Either remove the redundant check and comment, or make it explicit:

// git ls-tree already scopes output to dirPath; include every non-empty line.
for _, line := range lines {
    if line == "" {
        continue
    }
    files = append(files, line)
}

Alternatively, use strings.HasPrefix(line, dirPrefix) if you want a real guard against unexpected git output.

Comment thread pkg/cli/add_command.go Outdated
idx := strings.Index(resolved.Spec.WorkflowPath, skillComponent)
var relPath string
if idx >= 0 {
relPath = filepath.FromSlash(resolved.Spec.WorkflowPath[idx+len(skillComponent):])
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.

[/diagnose] When idx < 0 (the skill name doesn't appear as a path component in WorkflowPath), the fallback silently uses filepath.Base, which flattens any nested file to the skill root. This reproduces the exact silent-drop bug that this PR fixes — just in a subtler form.

💡 Suggestion

Return an error instead of silently falling back, so unexpected path shapes surface immediately:

if idx < 0 {
    return fmt.Errorf("skill %q: cannot determine relative path from WorkflowPath %q", resolved.SkillName, resolved.Spec.WorkflowPath)
}

A test case where WorkflowPath doesn't contain the skill name would also help pin down the contract.

Comment thread pkg/cli/add_package_manifest.go Outdated
return nil, nil, err
appendIfNew := func(dir string) {
name := filepath.Base(dir)
if _, exists := seenSkillNames[name]; !exists {
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.

[/zoom-out] appendIfNew deduplicates by filepath.Base(dir). If two different full paths share the same base name (e.g. a manifest entry vendor/skills/review and an auto-scanned skills/review), the second is silently skipped. This is unlikely in current usage but the silent discard contradicts the correctness focus of this PR.

💡 Suggestion

Deduplicate on the full canonical path rather than the base name, or document the assumption that base names are unique across all skill dirs:

// appendIfNew deduplicates by full dir path.
appendIfNew := func(dir string) {
    if _, exists := seenSkillNames[dir]; !exists {
        seenSkillNames[dir] = struct{}{}
        skillDirs = append(skillDirs, dir)
    }
}

The skill name for display/install can still be derived from filepath.Base(dir) downstream.

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 77/100 — Acceptable

Analyzed 20 tests (across 6 new/modified test functions): 20 design tests, 0 implementation tests, 0 hard guideline violations; test-to-production-file ratio slightly exceeds the 2:1 threshold, and assertion messages are missing throughout.

📊 Metrics & Test Classification (20 tests analyzed)
Metric Value
New/modified tests analyzed 20
✅ Design tests (behavioral contracts) 20 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 18 (90%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes — add_package_manifest_test.go (+142 lines) vs add_package_manifest.go (+50 lines) = 2.84:1 ratio
🚨 Coding-guideline violations 0 hard violations; ⚠️ soft: missing assertion messages on most assert.* calls

Test Classification Details

Test File Classification Issues Detected
TestIsSupportedSkillDirPath (13 table rows) pkg/cli/add_package_manifest_test.go:915 ✅ Design Covers edge cases (traversal, empty, wrong prefix) — good. No assertion messages.
TestIsSupportedAgentFilePath (11 table rows) pkg/cli/add_package_manifest_test.go:945 ✅ Design Covers invalid extensions, nested dirs, traversal. No assertion messages.
TestExtractManifestSkillDirs — valid entries pkg/cli/add_package_manifest_test.go:977 ✅ Design Happy-path verifying output list.
TestExtractManifestSkillDirs — invalid entries produce warnings pkg/cli/add_package_manifest_test.go:983 ✅ Design Error/rejection path.
TestExtractManifestSkillDirs — duplicate entries pkg/cli/add_package_manifest_test.go:990 ✅ Design Deduplication invariant.
TestExtractManifestSkillDirs — non-list value pkg/cli/add_package_manifest_test.go:996 ✅ Design Type-error path.
TestExtractManifestAgentFiles — valid entries pkg/cli/add_package_manifest_test.go:1005 ✅ Design Happy-path.
TestExtractManifestAgentFiles — invalid entries pkg/cli/add_package_manifest_test.go:1011 ✅ Design Rejection/warning path.
TestExtractManifestAgentFiles — duplicates pkg/cli/add_package_manifest_test.go:1019 ✅ Design Deduplication invariant.
TestExtractManifestAgentFiles — non-list pkg/cli/add_package_manifest_test.go:1025 ✅ Design Type-error path.
TestResolveRepositoryPackage_SkillsAndAgents — explicit skills/agents from manifest pkg/cli/add_package_manifest_test.go:1055 ✅ Design Key behavioral contract for manifest-driven skill copy. No assertion messages.
TestResolveRepositoryPackage_SkillsAndAgents — includes field infers types pkg/cli/add_package_manifest_test.go:1102 ✅ Design Behavioral.
TestResolveRepositoryPackage_SkillsAndAgents — auto-scans when absent pkg/cli/add_package_manifest_test.go:1142 ✅ Design Behavioral fallback path.
TestResolveRepositoryPackage_SkillsAndAgents — missing skill dir is warning pkg/cli/add_package_manifest_test.go:1193 ✅ Design Error-handling contract.
TestResolveRepositoryPackage_SkillsAndAgents — no SKILL.md marker pkg/cli/add_package_manifest_test.go:1231 ✅ Design Error-handling contract.
TestResolveRepositoryPackage_SkillsAndAgents — no dirs absent pkg/cli/add_package_manifest_test.go:1270 ✅ Design Boundary: no skills found.
TestResolveRepositoryPackage_SkillsAndAgents — manifest first then auto-scan pkg/cli/add_package_manifest_test.go:1305 ✅ Design Core PR invariant: manifest skills appear before auto-scanned; ordering is verified.
TestResolveRepositoryPackage_SkillsAndAgents — nested files recursive pkg/cli/add_package_manifest_test.go:1361 ✅ Design Core PR invariant: recursive listing of skill folder contents.
TestResolveWorkflows_SkillsAndAgents pkg/cli/add_package_manifest_test.go:1415 ✅ Design Integration: full resolve pipeline includes skill and agent files with correct flags.
TestResolveRepositoryPackage — uses aw manifest (modified) pkg/cli/add_package_manifest_test.go:45 ✅ Design Manifest route verified; README path and warnings checked.

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 20 tests — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests
⚠️ Soft Findings — No Hard Failures (1 pattern)

⚠️ Missing assertion messages (soft guideline violation, widespread)

Pattern: Nearly every assert.Equal, assert.Contains, assert.Empty, etc. call is written without a descriptive message argument. Examples:

assert.Equal(t, "aw.yml", pkg.ManifestPath)              // no message
assert.Equal(t, tt.want, isSupportedSkillDirPath(tt.path)) // no message

Project guideline requires: assert.Equal(t, expected, actual, "context description").

Impact: When a test fails, the output prints only the mismatched values with no context — making it harder to diagnose failures at a glance, especially in table-driven loops.

Suggested fix: Add a descriptive string as the third argument. In table-driven tests, the subtest name (tt.name via t.Run) often provides enough context to make the omission acceptable, but individual assertions in multi-step tests should still carry a message.

This is a soft finding — not a hard-fail condition — but is worth addressing in this PR or a follow-up since the newly added tests establish the pattern for future contributors.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). Build tag present, no mock libraries used. Score 77/100 — acceptable. Minor note: consider adding assertion messages and note the 2.84:1 test-to-production-file ratio (borderline, likely explained by new tests exercising helpers across multiple production files).

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 2.1M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 77/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). All 20 new tests are behavioral design tests with strong edge-case coverage. Build tag present, no mock libraries used.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

REQUEST_CHANGES — one security bug and one behavioural regression must be fixed before merge.

### Blocking issues (3)
  1. Path traversal (add_command.go) — relPath sliced from a remote WorkflowPath is passed directly to filepath.Join(skillDir, relPath). filepath.Join cleans .. but does not clamp within skillDir; a crafted path like skills/myskill/../../.github/workflows/evil.yml overwrites arbitrary files in the repo. Add a strings.HasPrefix guard after Join.

  2. Unconditional scanPackageSkillDirs (add_package_manifest.go) — was previously skipped when manifest skills were provided; now always runs. A transient API error (auth, rate limit) will fail the entire add command for packages that previously worked fine with explicit skill dirs only. Treat auto-scan errors as non-fatal warnings.

  3. Unbounded recursion (remote_fetch.go) — listContentsRecursively has no depth limit; a deeply nested or adversarial package causes stack exhaustion. Add a maxDepth guard (10 levels is more than sufficient).

### Non-blocking (1)
  • strings.Index first-match (add_command.go) — finds the first /skillName/ in the path, which misidentifies the skill root if the same name appears earlier as a path component. Low probability but silent wrong-path bug; a path-split approach is safer.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 2M

Comment thread pkg/cli/add_command.go Outdated
var relPath string
if idx >= 0 {
relPath = filepath.FromSlash(resolved.Spec.WorkflowPath[idx+len(skillComponent):])
} else {
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.

Path traversal: relPath from a remote URL can escape skillDir and write files anywhere on disk.

💡 Details and fix

filepath.Join resolves .. segments but does NOT clamp the result inside skillDir. A WorkflowPath like skills/myskill/../../.github/workflows/evil.yml produces a relPath of ../../.github/workflows/evil.yml, and filepath.Join(skillDir, relPath) happily produces a path two levels above skillDir.

Since WorkflowPath comes from a remote GitHub Contents API response (untrusted data), an adversarial or compromised package could overwrite arbitrary files in the local repo.

Suggested fix — validate the final path stays inside skillDir:

destFile := filepath.Join(skillDir, relPath)
if !strings.HasPrefix(destFile, skillDir+string(os.PathSeparator)) {
    return fmt.Errorf("skill file path %q escapes skill directory %q", relPath, skillDir)
}

Comment thread pkg/cli/add_package_manifest.go Outdated
func resolvePackageSkillFiles(owner, repo, packagePath, ref, host string, explicitSkillDirs []string) ([]resolvedPackageSkillFile, []string, error) {
// seenSkillNames tracks skill names already added so that auto-scanned duplicates
// of manifest-specified skills are not added a second time.
seenSkillNames := make(map[string]struct{})
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.

Regression: scanPackageSkillDirs now runs unconditionally, breaking packages that use only manifest skills when the skills/ directory is absent or the API call fails.

💡 Details and fix

Previously, scanPackageSkillDirs was only called when explicitSkillDirs was empty. Now it always runs. If the call returns any non-404 error (auth failure, rate limit, network timeout), the entire resolvePackageSkillFiles call fails — even for packages whose manifests fully enumerate their skills and don't need auto-scanning at all.

This is a behavioural regression: adding a package that worked fine before (explicit skills, no skills/ dir) could now fail intermittently due to transient API errors on an API call that wasn't previously made.

Suggested fix — propagate scanPackageSkillDirs errors as warnings (not hard errors), since auto-scan is supplementary:

autoScanned, scanErr := scanPackageSkillDirs(owner, repo, packagePath, ref, host)
if scanErr != nil {
    // Auto-scan is best-effort; log the error but don't block manifest skills.
    warnings = append(warnings, fmt.Sprintf("auto-scan of skills/ failed: %v", scanErr))
}

Or guard it: only auto-scan when explicitSkillDirs is empty.

case "dir":
subFiles, err := listContentsRecursively(client, owner, repo, ref, item.Path)
if err != nil {
return nil, err
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.

Unbounded recursion in listContentsRecursively can exhaust stack or make thousands of serial API calls against a deep/adversarial directory tree.

💡 Details and fix

Each dir entry in the Contents API response triggers a recursive call with no depth limit. A skill folder with 10 levels of nesting makes 10 serial recursive calls; one with 100 levels causes a stack overflow. Since this function processes content from an external package (untrusted), a malicious or misconfigured package could cause the agent process to hang or crash.

Suggested fix — add a depth guard:

const maxRecursionDepth = 10

func listContentsRecursively(client *api.RESTClient, owner, repo, ref, dirPath string) ([]string, error) {
    return listContentsRecursivelyWithDepth(client, owner, repo, ref, dirPath, 0)
}

func listContentsRecursivelyWithDepth(client *api.RESTClient, owner, repo, ref, dirPath string, depth int) ([]string, error) {
    if depth > maxRecursionDepth {
        return nil, fmt.Errorf("skill directory %q exceeds maximum nesting depth %d", dirPath, maxRecursionDepth)
    }
    // ... existing code, passing depth+1 for recursive calls ...
}

Comment thread pkg/cli/add_command.go Outdated

// Determine the relative path of the file within the skill directory so that any
// nested subdirectories are preserved (e.g. "scripts/query.sh" stays under scripts/).
// We search for the skill name as a proper path component (bounded by "/") so that a
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.

strings.Index finds the first occurrence of the skill name as a path component, which can match the wrong segment if the name appears earlier in the path.

💡 Details and fix

If WorkflowPath is something like vendor/foo/bar/foo/file.yaml and SkillName is foo, strings.Index returns the position of the first /foo/ (inside vendor/foo/bar), producing a relPath of bar/foo/file.yaml instead of the intended file.yaml. The bug is silent — no error is returned, just a wrong destination path.

A safer approach splits on path separators:

parts := strings.Split(filepath.ToSlash(resolved.Spec.WorkflowPath), "/")
var relParts []string
for i, part := range parts {
    if part == resolved.SkillName && i+1 < len(parts) {
        relParts = parts[i+1:]
        break
    }
}
if len(relParts) > 0 {
    relPath = filepath.Join(relParts...)
} else {
    relPath = filepath.Base(resolved.Spec.WorkflowPath)
}

This matches on the exact path component rather than a substring.

@github-actions
Copy link
Copy Markdown
Contributor

@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · gpt54 11.5M ·

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments and reviews from copilot and GitHub-actions actor. Resolve.

Copilot AI and others added 2 commits May 30, 2026 16:03
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 30, 2026

@copilot review all comments and reviews from copilot and GitHub-actions actor. Resolve.

Addressed the unresolved Copilot/github-actions feedback in commits d3ebb31 and 35f97af.

Resolved items:

  • Added path-traversal protection and strict skill-relative path derivation in addSkillFileWithTracking (no flattening fallback).
  • Made auto-scan failures non-fatal when manifest skills are explicitly listed, with warnings.
  • Added recursion depth guard for recursive remote directory listing.
  • Simplified recursive git fallback listing behavior and tightened dedup handling.
  • Added/updated regression tests for path handling, traversal rejection, auto-scan warning behavior, and depth guard.

Copilot AI changed the title Copy skills from aw.yml manifest first; copy entire skill folders recursively Copy skills from aw.yml manifest first; copy skill folders recursively and safely May 30, 2026
@pelikhan pelikhan merged commit 654a2cb into main May 30, 2026
26 checks passed
@pelikhan pelikhan deleted the copilot/update-skill-copy-logic branch May 30, 2026 16:39
@github-actions
Copy link
Copy Markdown
Contributor

@copilot summarize the remaining blockers and rerun checks.

Generated by 👨‍🍳 PR Sous Chef · gpt54 24.3M ·

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.

3 participants