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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .beads/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ daemon.log
daemon.pid
bd.sock
sync-state.json
last-touched

# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
Expand All @@ -19,6 +20,10 @@ sync-state.json
db.sqlite
bd.db

# Worktree redirect file (contains relative path to main repo's .beads/)
# Must not be committed as paths would be wrong in other clones
redirect

# Merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.base.meta.json
Expand All @@ -27,8 +32,8 @@ beads.left.meta.json
beads.right.jsonl
beads.right.meta.json

# Keep JSONL exports and config (source of truth for git)
!issues.jsonl
!interactions.jsonl
!metadata.json
!config.json
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
# They would override fork protection in .git/info/exclude, allowing
# contributors to accidentally commit upstream issue databases.
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
# are tracked by git by default since no pattern above ignores them.
1 change: 1 addition & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
{"id":"Grace-f1u.1","title":"Add PAT SDK wrappers","description":"Implement Grace.SDK PersonalAccessToken wrappers for create/list/revoke and update project compile list.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:35:17.2437547-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:49:41.0114696-08:00","closed_at":"2026-01-01T17:49:41.0114696-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-f1u.1","depends_on_id":"Grace-f1u","type":"parent-child","created_at":"2026-01-01T17:35:17.2831975-08:00","created_by":"daemon"}]}
{"id":"Grace-f1u.2","title":"Ensure PAT SDK uses GRACE_SERVER_URI","description":"Make PAT SDK endpoints work without graceconfig.json by using GRACE_SERVER_URI or a helper that bypasses Configuration.Current().","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:35:24.0343531-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:49:49.2757481-08:00","closed_at":"2026-01-01T17:49:49.2757481-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-f1u.2","depends_on_id":"Grace-f1u","type":"parent-child","created_at":"2026-01-01T17:35:24.0786964-08:00","created_by":"daemon"}]}
{"id":"Grace-f2t","title":"Server: derived computation trigger consumer","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-06T01:14:49.3862902-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-06T01:44:59.6212182-08:00","closed_at":"2026-01-06T01:44:59.6212182-08:00","close_reason":"Closed"}
{"id":"Grace-far","title":"Allow grace connect without config when parse errors occur","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-09T02:38:47.1058823-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-09T02:40:29.8344153-08:00","closed_at":"2026-01-09T02:40:29.8344153-08:00","close_reason":"Closed"}
{"id":"Grace-g11","title":"SDK: Queue/Candidate APIs","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-06T01:14:54.2524092-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-06T02:56:38.0685742-08:00","closed_at":"2026-01-06T02:56:38.0685742-08:00","close_reason":"Closed"}
{"id":"Grace-g4d","title":"Types: Work Item contracts","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-06T01:14:47.0471393-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-06T01:20:50.4717585-08:00","closed_at":"2026-01-06T01:20:50.4717585-08:00","close_reason":"Closed"}
{"id":"Grace-hj3","title":"Types: RequiredAction + Event envelope contracts","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-06T01:14:48.2364827-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-06T01:29:04.1667329-08:00","closed_at":"2026-01-06T01:29:04.1667329-08:00","close_reason":"Closed"}
Expand Down
2 changes: 1 addition & 1 deletion .beads/last-touched
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Grace-4g4
Grace-far
38 changes: 38 additions & 0 deletions .grace/graceconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"OwnerName": "",
"OrganizationName": "",
"RepositoryName": "",
"BranchName": "",
"DefaultBranchName": "",
"Themes": [
{
"Name": "Default",
"DisplayColorOptions": {
"added": "#00AF5F",
"changed": "#800080",
"deemphasized": "#808080",
"deleted": "#8B0000",
"error": "#FF0000",
"highlighted": "#FFFFFF",
"important": "#E5C07B",
"verbose": "#FF7E00"
}
}
],
"LineEndings": "PlatformDependent",
"Prefetch": [
""
],
"RootDirectory": "D:\\Source\\Grace",
"StandardizedRootDirectory": "D:/Source/Grace",
"GraceDirectory": "D:\\Source\\Grace",
"ObjectDirectory": "D:\\Source\\Grace",
"GraceStatusFile": "gracestatus.msgpack",
"GraceObjectCacheFile": "graceObjectCache.msgpack",
"DirectoryVersionCache": "D:\\Source\\Grace",
"ConfigurationDirectory": "D:\\Source\\Grace",
"ObjectStorageProvider": "unknown",
"ServerUri": "http://127.0.0.1:5000",
"ProgramVersion": "0.1",
"ConfigurationVersion": ""
}
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Agent Instructions

Other `AGENTS.md` files exist in subdirectories, refer to them for more specific context.
Other `AGENTS.md` files exist in subdirectories, refer to them for more specific
context.

## Issue Tracking

This project uses **bd (beads)** for issue tracking.
Always use `bd` commands to manage your work.
Run `bd prime` for workflow context, or install hooks (`bd hooks install`) for auto-injection.

**Quick reference:**
Expand Down
10 changes: 5 additions & 5 deletions src/Grace.Actors/Constants.Actor.fs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ module Constants =
[<Literal>]
let Branch = "Branch"

[<Literal>]
let ConflictReceipt = "ConflictReceipt"

[<Literal>]
let Diff = "Diff"

Expand All @@ -129,16 +132,13 @@ module Constants =
let FileAppearance = "FileApp"

[<Literal>]
let NamedSection = "NamedSection"
let GateAttestation = "GateAttestation"

[<Literal>]
let IntegrationCandidate = "IntegrationCandidate"

[<Literal>]
let GateAttestation = "GateAttestation"

[<Literal>]
let ConflictReceipt = "ConflictReceipt"
let NamedSection = "NamedSection"

[<Literal>]
let Organization = "Organization"
Expand Down
5 changes: 3 additions & 2 deletions src/Grace.Actors/DirectoryVersion.Actor.fs
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,9 @@ module DirectoryVersion =
tags.Add("RecursiveSize", $"{directoryVersionDto.RecursiveSize}")

// Write the JSON using MessagePack serialization for efficiency.
let blockBlobOpenWriteOptions = BlockBlobOpenWriteOptions(Tags = tags)
blockBlobOpenWriteOptions.HttpHeaders.ContentType <- "application/msgpack"
let blockBlobOpenWriteOptions =
BlockBlobOpenWriteOptions(Tags = tags, HttpHeaders = BlobHttpHeaders(ContentType = "application/msgpack"))

use! blobStream = directoryVersionBlobClient.OpenWriteAsync(overwrite = true, options = blockBlobOpenWriteOptions)
do! MessagePackSerializer.SerializeAsync(blobStream, subdirectoryVersionsList, messagePackSerializerOptions)
do! blobStream.DisposeAsync()
Expand Down
3 changes: 3 additions & 0 deletions src/Grace.Aspire.AppHost/Program.Aspire.AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ private static void Main(string[] args)
.WithEnvironment(EnvironmentVariables.AzureServiceBusTopic, configuration["Grace:ServiceBus:TopicName"] ?? "graceeventstream")
.WithEnvironment(EnvironmentVariables.AzureServiceBusSubscription, configuration["Grace:ServiceBus:SubscriptionName"] ?? "grace-server")
.WithEnvironment(EnvironmentVariables.GraceLogDirectory, publishLogDirectory)
.WithEnvironment(EnvironmentVariables.GraceAuthOidcAuthority, configuration[EnvironmentVariables.GraceAuthOidcAuthority])
.WithEnvironment(EnvironmentVariables.GraceAuthOidcAudience, configuration[EnvironmentVariables.GraceAuthOidcAudience])
.WithEnvironment(EnvironmentVariables.GraceAuthOidcCliClientId, configuration[EnvironmentVariables.GraceAuthOidcCliClientId])
.WithHttpEndpoint(targetPort: 5000, name: "http")
.WithHttpsEndpoint(targetPort: 5001, name: "https")
.AsHttp2Service()
Expand Down
3 changes: 2 additions & 1 deletion src/Grace.CLI.Tests/Auth.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ module AuthTests =
withEnv Constants.EnvironmentVariables.GraceAuthOidcAudience None (fun () ->
withEnv Constants.EnvironmentVariables.GraceAuthOidcCliClientId None (fun () ->
withEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientId None (fun () ->
withEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientSecret None action))))
withEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientSecret None (fun () ->
withEnv Constants.EnvironmentVariables.GraceServerUri None action)))))

[<Test>]
let ``tryGetAccessToken returns Error when auth is not configured`` () =
Expand Down
31 changes: 31 additions & 0 deletions src/Grace.CLI.Tests/CommandParsing.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Grace.CLI.Tests

open FsUnit
open Grace.CLI
open NUnit.Framework

[<TestFixture>]
module CommandParsingTests =
[<Test>]
let ``top level command returns none for empty args`` () =
GraceCommand.tryGetTopLevelCommandFromArgs Array.empty true |> should equal None

[<Test>]
let ``top level command detects command token`` () =
GraceCommand.tryGetTopLevelCommandFromArgs [| "connect"; "owner/org/repo" |] true
|> should equal (Some "connect")

[<Test>]
let ``top level command skips output option`` () =
GraceCommand.tryGetTopLevelCommandFromArgs [| "--output"; "Verbose"; "connect" |] true
|> should equal (Some "connect")

[<Test>]
let ``top level command skips correlation id option`` () =
GraceCommand.tryGetTopLevelCommandFromArgs [| "-c"; "abc123"; "connect" |] true
|> should equal (Some "connect")

[<Test>]
let ``top level command honors end of options marker`` () =
GraceCommand.tryGetTopLevelCommandFromArgs [| "--output"; "Verbose"; "--"; "connect" |] true
|> should equal (Some "connect")
138 changes: 138 additions & 0 deletions src/Grace.CLI.Tests/Connect.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
namespace Grace.CLI.Tests

open FsUnit
open Grace.CLI
open Grace.CLI.Command
open Grace.CLI.Text
open Grace.Shared.Client.Configuration
open Grace.Types.Branch
open Grace.Types.Reference
open Grace.Types.Types
open NUnit.Framework
open Spectre.Console
open System
open System.IO

[<NonParallelizable>]
module ConnectTests =
let private setAnsiConsoleOutput (writer: TextWriter) =
let settings = AnsiConsoleSettings()
settings.Out <- AnsiConsoleOutput(writer)
AnsiConsole.Console <- AnsiConsole.Create(settings)
let private runWithCapturedOutput (args: string array) =
use writer = new StringWriter()
let originalOut = Console.Out

try
Console.SetOut(writer)
setAnsiConsoleOutput writer
let exitCode = GraceCommand.main args
exitCode, writer.ToString()
finally
Console.SetOut(originalOut)
setAnsiConsoleOutput originalOut

let private withTempDir (action: string -> unit) =
let tempDir = Path.Combine(Path.GetTempPath(), $"grace-cli-tests-{Guid.NewGuid():N}")
Directory.CreateDirectory(tempDir) |> ignore
let originalDir = Environment.CurrentDirectory

try
Environment.CurrentDirectory <- tempDir
action tempDir
finally
Environment.CurrentDirectory <- originalDir

if Directory.Exists(tempDir) then
try
Directory.Delete(tempDir, true)
with _ ->
()

let private getGraceConfigPath root = Path.Combine(root, ".grace", "graceconfig.json")

[<Test>]
let ``connect creates config when missing`` () =
withTempDir (fun root ->
let exitCode, _ = runWithCapturedOutput [| "connect" |]
exitCode |> should equal -1
File.Exists(getGraceConfigPath root) |> should equal true)

[<Test>]
let ``connect retrieve default branch defaults to true`` () =
let parseResult = GraceCommand.rootCommand.Parse([| "connect" |])

parseResult.GetValue<bool>(OptionName.RetrieveDefaultBranch)
|> should equal true

[<Test>]
let ``connect retrieve default branch parses explicit false`` () =
let parseResult = GraceCommand.rootCommand.Parse([| "connect"; OptionName.RetrieveDefaultBranch; "false" |])

parseResult.GetValue<bool>(OptionName.RetrieveDefaultBranch)
|> should equal false

[<Test>]
let ``connect directory version selection precedence uses directory version id`` () =
let directoryVersionId = Guid.NewGuid()
let referenceId = Guid.NewGuid()

let parseResult =
GraceCommand.rootCommand.Parse(
[| "connect"
OptionName.DirectoryVersionId
$"{directoryVersionId}"
OptionName.ReferenceId
$"{referenceId}"
OptionName.ReferenceType
"Commit" |]
)

match Connect.getDirectoryVersionSelection parseResult with
| Connect.UseDirectoryVersionId selected -> selected |> should equal directoryVersionId
| other -> Assert.Fail($"Unexpected selection: {other}")

[<Test>]
let ``connect default directory version falls back to based-on`` () =
let basedOnId = Guid.NewGuid()
let branchDto = { BranchDto.Default with LatestPromotion = ReferenceDto.Default; BasedOn = { ReferenceDto.Default with DirectoryId = basedOnId } }

Connect.resolveDefaultDirectoryVersionId branchDto
|> should equal (Some basedOnId)

[<Test>]
let ``connect repository shortcut populates owner organization repository`` () =
let parseResult = GraceCommand.rootCommand.Parse([| "connect"; "owner/org/repo" |])
let graceIds = GraceIds.Default

match Connect.applyRepositoryShortcut parseResult graceIds with
| Ok updated ->
updated.OwnerName |> should equal "owner"
updated.OrganizationName |> should equal "org"
updated.RepositoryName |> should equal "repo"
updated.OwnerId |> should equal Guid.Empty
updated.OrganizationId |> should equal Guid.Empty
updated.RepositoryId |> should equal Guid.Empty
updated.HasOwner |> should equal true
updated.HasOrganization |> should equal true
updated.HasRepository |> should equal true
| Error error -> Assert.Fail($"Unexpected error: {error.Error}")

[<Test>]
let ``connect repository shortcut rejects missing segments`` () =
let parseResult = GraceCommand.rootCommand.Parse([| "connect"; "owner/repo" |])
let graceIds = GraceIds.Default

match Connect.applyRepositoryShortcut parseResult graceIds with
| Ok _ -> Assert.Fail("Expected error when repository shortcut is missing segments.")
| Error error -> error.Error |> should contain "owner/organization/repository"

[<Test>]
let ``connect repository shortcut conflicts with explicit options`` () =
let parseResult = GraceCommand.rootCommand.Parse([| "connect"; "owner/org/repo"; OptionName.OwnerName; "explicit-owner" |])

let graceIds = GraceIds.Default

match Connect.applyRepositoryShortcut parseResult graceIds with
| Ok _ -> Assert.Fail("Expected error when shortcut is combined with explicit options.")
| Error error -> error.Error |> should contain "Provide either the repository shortcut"
2 changes: 2 additions & 0 deletions src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<ItemGroup>
<Compile Include="Auth.Tests.fs" />
<Compile Include="AuthTokenBundle.Tests.fs" />
<Compile Include="Connect.Tests.fs" />
<Compile Include="CommandParsing.Tests.fs" />
<Compile Include="HelpDoesNotReadConfig.Tests.fs" />
<Compile Include="HistoryStorage.Tests.fs" />
<Compile Include="WorkItem.Commands.Tests.fs" />
Expand Down
Loading