Skip to content

feat(workspace): add project detection for workspace grouping#91

Merged
feloy merged 1 commit intokortex-hub:mainfrom
feloy:git-repo
Mar 23, 2026
Merged

feat(workspace): add project detection for workspace grouping#91
feloy merged 1 commit intokortex-hub:mainfrom
feloy:git-repo

Conversation

@feloy
Copy link
Copy Markdown
Contributor

@feloy feloy commented Mar 23, 2026

Implements project abstraction that allows grouping workspaces belonging to the same project across different branches, forks, or subdirectories.

The project identifier is automatically detected from:

  • Git repository with remote: uses upstream (or origin) URL + relative path
  • Git repository without remote: uses repository root + relative path
  • Non-git directory: uses source directory path

Users can override auto-detection with the --project flag to specify custom identifiers for organizational needs (e.g., team names, clients).

Closes #89, #41

This will be needed for giving specific user configuration per project (see #88)

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 75.00000% with 19 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/git/git.go 76.31% 6 Missing and 3 partials ⚠️
pkg/git/exec.go 0.00% 6 Missing ⚠️
pkg/instances/instance.go 50.00% 2 Missing ⚠️
pkg/instances/manager.go 91.66% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

Warning

Rate limit exceeded

@feloy has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 46 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: debe3805-ee44-486d-8693-930fdc729124

📥 Commits

Reviewing files that changed from the base of the PR and between 4edd814 and 0aa011a.

📒 Files selected for processing (11)
  • AGENTS.md
  • README.md
  • pkg/cmd/init.go
  • pkg/cmd/init_test.go
  • pkg/git/exec.go
  • pkg/git/git.go
  • pkg/git/git_test.go
  • pkg/instances/instance.go
  • pkg/instances/manager.go
  • pkg/instances/manager_project_test.go
  • pkg/instances/manager_test.go
📝 Walkthrough

Walkthrough

Adds project detection and grouping: auto-detects a workspace project from git (preferring upstream, then origin, stripping .git, appending relative path) or falls back to the source directory; introduces a CLI --project override; adds git executor/detector abstractions and persists project on instances.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md, README.md
Adds "Project Detection and Grouping" docs, documents auto-detection rules and the new kortex-cli init --project, -p override, and updates examples/notes.
CLI Command
pkg/cmd/init.go, pkg/cmd/init_test.go
Adds project string to init command, new --project/-p flag, passes Project into instances.AddOptions, updates examples and E2E test verifying GetProject().
Git execution & detection
pkg/git/exec.go, pkg/git/git.go, pkg/git/git_test.go
Introduces Executor interface and concrete executor for running git commands; adds Detector to detect repo root, remote (prefers upstream, then origin) and relative path; defines ErrNotGitRepository; includes comprehensive tests with fake executor.
Instance model & manager
pkg/instances/instance.go, pkg/instances/manager.go, pkg/instances/manager_test.go, pkg/instances/manager_project_test.go
Adds Project field to persisted InstanceData and GetProject() API; extends AddOptions with Project; injects git.Detector into manager; implements manager.detectProject(...) (remote URL + relative path or filesystem fallback); updates and adds tests and test doubles.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as "CLI (init)"
    participant Manager
    participant Detector as "Git Detector"
    participant Exec as "Git Executor"

    User->>CLI: run kortex-cli init [--project custom]
    CLI->>Manager: Add(opts{Project: "custom"/"" , sourceDir})
    alt custom project provided
        Manager->>Manager: set instance.Project = opts.Project
    else auto-detect
        Manager->>Detector: DetectRepository(ctx, sourceDir)
        Detector->>Exec: git rev-parse --show-toplevel
        Exec-->>Detector: root dir or exit 128
        alt repo detected
            Detector->>Exec: git remote get-url upstream
            Exec-->>Detector: remote URL / error
            Detector->>Exec: git remote get-url origin
            Exec-->>Detector: remote URL / error
            Detector-->>Manager: RepositoryInfo{RootDir, RemoteURL, RelativePath}
            Manager->>Manager: build Project = normalize(RemoteURL) + "/" + RelativePath
        else not a git repo
            Detector-->>Manager: ErrNotGitRepository
            Manager->>Manager: Project = sourceDir
        end
    end
    Manager->>Manager: create instance with Project
    Manager-->>CLI: instance created
    CLI-->>User: workspace registered
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(workspace): add project detection for workspace grouping' clearly and concisely summarizes the primary feature being added—project detection for grouping workspaces.
Description check ✅ Passed The description provides relevant context about the implementation, including auto-detection rules, CLI override capability, and related issue references.
Linked Issues check ✅ Passed The PR fully implements the project abstraction requirements from #89: git repos with remotes use remote URL plus relative path, repos without remotes use root plus relative path, non-git directories use source path, and users can override with --project flag.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing project detection and grouping: git detection abstractions, project storage/retrieval, CLI flag, documentation, and comprehensive tests—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/instances/manager.go (2)

273-284: ⚠️ Potential issue | 🟠 Major

Project field not preserved when starting instance.

The updatedInstance is missing the Project field. After Start() is called, the persisted instance will lose its project association.

🐛 Proposed fix
 	// Update the instance with new runtime state
 	updatedInstance := &instance{
 		ID:        instanceToStart.GetID(),
 		Name:      instanceToStart.GetName(),
 		SourceDir: instanceToStart.GetSourceDir(),
 		ConfigDir: instanceToStart.GetConfigDir(),
 		Runtime: RuntimeData{
 			Type:       runtimeData.Type,
 			InstanceID: runtimeData.InstanceID,
 			State:      runtimeInfo.State,
 			Info:       runtimeInfo.Info,
 		},
+		Project: instanceToStart.GetProject(),
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/instances/manager.go` around lines 273 - 284, The new updatedInstance
constructed in Start() omits the Project field so the persisted instance loses
its project association; update the construction of updatedInstance (created
from instanceToStart in Start/manager.go) to include Project:
instanceToStart.GetProject() (or the equivalent Project value from
instanceToStart) so the instance struct's Project is preserved when calling
Start() and saving the updated instance.

341-352: ⚠️ Potential issue | 🟠 Major

Project field not preserved when stopping instance.

Same issue as Start() — the Project field is omitted from updatedInstance, causing data loss on save.

🐛 Proposed fix
 	// Update the instance with new runtime state
 	updatedInstance := &instance{
 		ID:        instanceToStop.GetID(),
 		Name:      instanceToStop.GetName(),
 		SourceDir: instanceToStop.GetSourceDir(),
 		ConfigDir: instanceToStop.GetConfigDir(),
 		Runtime: RuntimeData{
 			Type:       runtimeData.Type,
 			InstanceID: runtimeData.InstanceID,
 			State:      runtimeInfo.State,
 			Info:       runtimeInfo.Info,
 		},
+		Project: instanceToStop.GetProject(),
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/instances/manager.go` around lines 341 - 352, The Stop() flow constructs
updatedInstance without copying the Project field, causing data loss on save;
update the construction of updatedInstance (the instance literal created in
Stop(), same pattern as Start()) to include Project: instanceToStop.GetProject()
(or the appropriate accessor) so the Project value is preserved in the
Runtime/instance struct before saving.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/instances/manager.go`:
- Around line 524-528: Normalize repoInfo.RelativePath to use forward slashes
before concatenating with baseURL to avoid backslash URL bugs on Windows:
replace the direct concatenation return baseURL + repoInfo.RelativePath with
normalization (e.g., use filepath.ToSlash(repoInfo.RelativePath) or
strings.ReplaceAll with string(os.PathSeparator) -> "/") and then append that
normalized value to baseURL; ensure you import the needed package (path/filepath
or strings/os) and keep the comment about avoiding path.Join.

---

Outside diff comments:
In `@pkg/instances/manager.go`:
- Around line 273-284: The new updatedInstance constructed in Start() omits the
Project field so the persisted instance loses its project association; update
the construction of updatedInstance (created from instanceToStart in
Start/manager.go) to include Project: instanceToStart.GetProject() (or the
equivalent Project value from instanceToStart) so the instance struct's Project
is preserved when calling Start() and saving the updated instance.
- Around line 341-352: The Stop() flow constructs updatedInstance without
copying the Project field, causing data loss on save; update the construction of
updatedInstance (the instance literal created in Stop(), same pattern as
Start()) to include Project: instanceToStop.GetProject() (or the appropriate
accessor) so the Project value is preserved in the Runtime/instance struct
before saving.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b80fb989-992d-4cc2-a89e-f2bb58eaf083

📥 Commits

Reviewing files that changed from the base of the PR and between 071ae05 and 6c27d84.

📒 Files selected for processing (11)
  • AGENTS.md
  • README.md
  • pkg/cmd/init.go
  • pkg/cmd/init_test.go
  • pkg/git/exec.go
  • pkg/git/git.go
  • pkg/git/git_test.go
  • pkg/instances/instance.go
  • pkg/instances/manager.go
  • pkg/instances/manager_project_test.go
  • pkg/instances/manager_test.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/git/git_test.go (1)

239-246: Consider using a simpler interface for exitError128.

The exitError128 type embeds exec.ExitError which has unexported fields that will be zero-valued. This works because the production code only calls ExitCode(), but embedding the full struct is unnecessary and could cause confusion.

♻️ Proposed simplification
 // exitError128 is a test helper that simulates git exit code 128
-type exitError128 struct {
-	exec.ExitError
-}
+type exitError128 struct{}

 func (e *exitError128) ExitCode() int {
 	return 128
 }
+
+func (e *exitError128) Error() string {
+	return "exit status 128"
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/git/git_test.go` around lines 239 - 246, The test helper exitError128
currently embeds exec.ExitError unnecessarily; replace the embedding with a
minimal type (e.g., type exitError128 struct{}) that implements the methods used
by production code: implement ExitCode() int returning 128 and also Error()
string to satisfy the error interface, removing the exec.ExitError embedding and
any dependence on its unexported fields; update any test construction or
assertions to use the new minimal exitError128 type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/git/git_test.go`:
- Around line 239-246: The test helper exitError128 currently embeds
exec.ExitError unnecessarily; replace the embedding with a minimal type (e.g.,
type exitError128 struct{}) that implements the methods used by production code:
implement ExitCode() int returning 128 and also Error() string to satisfy the
error interface, removing the exec.ExitError embedding and any dependence on its
unexported fields; update any test construction or assertions to use the new
minimal exitError128 type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27bf71a5-a03c-4241-a385-2fe49bfc982b

📥 Commits

Reviewing files that changed from the base of the PR and between 6c27d84 and fc3be2d.

📒 Files selected for processing (11)
  • AGENTS.md
  • README.md
  • pkg/cmd/init.go
  • pkg/cmd/init_test.go
  • pkg/git/exec.go
  • pkg/git/git.go
  • pkg/git/git_test.go
  • pkg/instances/instance.go
  • pkg/instances/manager.go
  • pkg/instances/manager_project_test.go
  • pkg/instances/manager_test.go
✅ Files skipped from review due to trivial changes (2)
  • pkg/git/exec.go
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/cmd/init.go
  • pkg/instances/manager_test.go
  • pkg/git/git.go
  • pkg/cmd/init_test.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
pkg/git/git.go (2)

84-90: Consider using errors.As() for more robust error unwrapping.

The direct type assertion err.(exitCoder) won't match if the error is wrapped. While this currently works with os/exec errors, using errors.As() is the idiomatic Go pattern and more resilient to future changes.

♻️ Suggested improvement
 	output, err := d.executor.Output(ctx, absDir, "rev-parse", "--show-toplevel")
 	if err != nil {
 		// Exit status 128 means not a git repository
-		if exitErr, ok := err.(exitCoder); ok && exitErr.ExitCode() == 128 {
+		var exitErr exitCoder
+		if errors.As(err, &exitErr) && exitErr.ExitCode() == 128 {
 			return nil, ErrNotGitRepository
 		}
 		return nil, err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/git/git.go` around lines 84 - 90, Replace the direct type assertion on
err with errors.As to correctly unwrap wrapped errors: when checking for the
exit code 128, declare a variable of type exitCoder (e.g., var exitErr
exitCoder) and use errors.As(err, &exitErr) to detect the underlying exitCoder,
then check exitErr.ExitCode() == 128 and return ErrNotGitRepository; update the
error handling branch in the code that currently does if err != nil { ... } so
it uses errors.As instead of err.(exitCoder).

107-123: Minor duplication in remote URL processing.

The trim and suffix removal logic is duplicated for both upstream and origin remotes. Consider extracting to reduce repetition, though this is a minor nitpick.

♻️ Optional refactor to reduce duplication
 	// Get the remote URL (try upstream first, then origin)
-	remoteURL := ""
-	output, err = d.executor.Output(ctx, rootDir, "remote", "get-url", "upstream")
-	if err == nil {
-		remoteURL = strings.TrimSpace(string(output))
-		// Remove .git suffix if present
-		remoteURL = strings.TrimSuffix(remoteURL, ".git")
-	} else {
-		// upstream not found, try origin
-		output, err = d.executor.Output(ctx, rootDir, "remote", "get-url", "origin")
-		if err == nil {
-			remoteURL = strings.TrimSpace(string(output))
-			// Remove .git suffix if present
-			remoteURL = strings.TrimSuffix(remoteURL, ".git")
+	remoteURL := d.getRemoteURL(ctx, rootDir, "upstream")
+	if remoteURL == "" {
+		remoteURL = d.getRemoteURL(ctx, rootDir, "origin")
+	}

Add a helper method:

// getRemoteURL returns the URL for the specified remote, or empty string if not found
func (d *detector) getRemoteURL(ctx context.Context, dir, remote string) string {
+	output, err := d.executor.Output(ctx, dir, "remote", "get-url", remote)
+	if err != nil {
+		return ""
+	}
+	url := strings.TrimSpace(string(output))
+	return strings.TrimSuffix(url, ".git")
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/git/git.go` around lines 107 - 123, The remote URL trimming/suffix
removal is duplicated; add a helper method named getRemoteURL on the detector
that calls d.executor.Output(ctx, dir, "remote", "get-url", remote), returns
empty string on error, otherwise returns
strings.TrimSuffix(strings.TrimSpace(string(output)), ".git"); then replace the
inline logic in the existing function to set remoteURL = d.getRemoteURL(ctx,
rootDir, "upstream") and if that returns empty call d.getRemoteURL(ctx, rootDir,
"origin") so all trimming/cleanup is centralized in getRemoteURL.
pkg/instances/instance.go (2)

165-168: Persistence and accessor wiring for Project is consistent.

GetProject(), Dump(), and NewInstanceFromData() align, which gives a solid round-trip path for stored project identifiers.

For reliability, please add a focused unit test for Project round-trip (instance -> Dump -> NewInstanceFromData -> GetProject) to guard regressions.

Also applies to: 180-180, 246-246

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/instances/instance.go` around lines 165 - 168, Add a focused unit test
that verifies the Project field round-trips through the instance serialization
cycle: create an instance (use the constructor or struct literal), set Project,
call instance.Dump(), reconstruct with NewInstanceFromData(dumpedData), and
assert reconstructed.GetProject() equals the original Project; reference
GetProject, Dump, and NewInstanceFromData in the test to catch regressions and
include edge cases like empty and non-empty project strings.

110-111: Consider whether Project should be passed to NewInstance for more explicit initialization.

Currently, Project is set during Manager.Add() when a new instance struct is created with the project identifier. While this works correctly (no out-of-band mutation occurs), passing Project through NewInstanceParams would make the factory's purpose clearer and reduce the multi-step initialization pattern.

This is an optional API improvement; the current design where Manager.Add() handles full instance initialization is sound.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/instances/instance.go` around lines 110 - 111, The Project field is
currently assigned in Manager.Add() after creating the instance; change
NewInstance to accept Project via NewInstanceParams so instances are fully
initialized by the factory. Update the NewInstance signature and
NewInstanceParams struct to include Project, propagate that new param through
calls from Manager.Add, and remove the out-of-factory assignment of
instance.Project so the factory (NewInstance) sets Project directly on the
instance struct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/instances/instance.go`:
- Around line 63-67: The struct field comment for Project on the Instance type
is outdated and should be updated to accurately describe the normalized project
identifier produced by the detection logic; update the comment for the Project
string `json:"project"` to describe the current formats: a normalized remote
repository identifier (remote URL normalized/stripped of `.git` and credentials,
not raw remote URL), a repository root path for local-only git repos (absolute
path), and a workspace source directory for non-git directories, and also
mention that a relative-path suffix may be appended when detection includes a
subpath; ensure the comment references the Project field and uses the same
terminology as the project-detection behavior in this PR.

---

Nitpick comments:
In `@pkg/git/git.go`:
- Around line 84-90: Replace the direct type assertion on err with errors.As to
correctly unwrap wrapped errors: when checking for the exit code 128, declare a
variable of type exitCoder (e.g., var exitErr exitCoder) and use errors.As(err,
&exitErr) to detect the underlying exitCoder, then check exitErr.ExitCode() ==
128 and return ErrNotGitRepository; update the error handling branch in the code
that currently does if err != nil { ... } so it uses errors.As instead of
err.(exitCoder).
- Around line 107-123: The remote URL trimming/suffix removal is duplicated; add
a helper method named getRemoteURL on the detector that calls
d.executor.Output(ctx, dir, "remote", "get-url", remote), returns empty string
on error, otherwise returns
strings.TrimSuffix(strings.TrimSpace(string(output)), ".git"); then replace the
inline logic in the existing function to set remoteURL = d.getRemoteURL(ctx,
rootDir, "upstream") and if that returns empty call d.getRemoteURL(ctx, rootDir,
"origin") so all trimming/cleanup is centralized in getRemoteURL.

In `@pkg/instances/instance.go`:
- Around line 165-168: Add a focused unit test that verifies the Project field
round-trips through the instance serialization cycle: create an instance (use
the constructor or struct literal), set Project, call instance.Dump(),
reconstruct with NewInstanceFromData(dumpedData), and assert
reconstructed.GetProject() equals the original Project; reference GetProject,
Dump, and NewInstanceFromData in the test to catch regressions and include edge
cases like empty and non-empty project strings.
- Around line 110-111: The Project field is currently assigned in Manager.Add()
after creating the instance; change NewInstance to accept Project via
NewInstanceParams so instances are fully initialized by the factory. Update the
NewInstance signature and NewInstanceParams struct to include Project, propagate
that new param through calls from Manager.Add, and remove the out-of-factory
assignment of instance.Project so the factory (NewInstance) sets Project
directly on the instance struct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 159aa613-5ab6-4195-bedd-b982768c62d1

📥 Commits

Reviewing files that changed from the base of the PR and between fc3be2d and 4edd814.

📒 Files selected for processing (11)
  • AGENTS.md
  • README.md
  • pkg/cmd/init.go
  • pkg/cmd/init_test.go
  • pkg/git/exec.go
  • pkg/git/git.go
  • pkg/git/git_test.go
  • pkg/instances/instance.go
  • pkg/instances/manager.go
  • pkg/instances/manager_project_test.go
  • pkg/instances/manager_test.go
✅ Files skipped from review due to trivial changes (3)
  • README.md
  • pkg/git/exec.go
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • pkg/cmd/init.go
  • pkg/cmd/init_test.go
  • pkg/git/git_test.go
  • pkg/instances/manager_project_test.go
  • pkg/instances/manager_test.go
  • pkg/instances/manager.go

Implements project abstraction that allows grouping workspaces belonging
to the same project across different branches, forks, or subdirectories.

The project identifier is automatically detected from:
- Git repository with remote: uses upstream (or origin) URL + relative path
- Git repository without remote: uses repository root + relative path
- Non-git directory: uses source directory path

Users can override auto-detection with the --project flag to specify
custom identifiers for organizational needs (e.g., team names, clients).

Closes kortex-hub#89, kortex-hub#41

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
@feloy feloy enabled auto-merge (squash) March 23, 2026 15:17
@feloy feloy merged commit a5d4c31 into kortex-hub:main Mar 23, 2026
6 checks passed
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.

project abstraction

3 participants