A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
// brokkr-command: a caller. It writes one Unity command intent into the project's directory store and reads the
// receipt the editor service answers with. It never writes receipts, host snapshots or the agent-commands flag,
// and it never opens the store as a single file: the editor owns execution and its own state.
using System.Globalization ;
using GameCult.Brokkr ;
using GameCult.Caching ;
using GameCult.Caching.MessagePack ;
2026-09-29 16:16:50 +02:00
// Exit codes: 0 the receipt was accepted (or readHost / an unwaited write succeeded); 1 the receipt says the editor
// did not accept it (failed, denied, expired, interrupted); 2 anything else: bad usage, a timeout, an unreadable store.
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
try
{
return await Cli . RunAsync ( args ) ;
}
catch ( Exception error ) when ( error is ArgumentException or TimeoutException or InvalidOperationException )
{
Console . Error . WriteLine ( error . Message ) ;
return 2 ;
}
2026-09-29 16:16:50 +02:00
catch ( Exception error )
{
Console . Error . WriteLine ( $"brokkr-command failed: {error.GetType().Name}: {error.Message}" ) ;
return 2 ;
}
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
internal static class Cli
{
// Every writable field of brokkr.unity.command_intent.v0 except the schema tag, the id and the action, which
// have their own options.
private static readonly Dictionary < string , Action < BrokkrUnityCommand , string > > Fields =
new ( StringComparer . Ordinal )
{
["--target-object-id"] = ( command , value ) = > command . targetObjectId = value ,
["--name"] = ( command , value ) = > command . name = value ,
["--component-type"] = ( command , value ) = > command . componentType = value ,
["--property-path"] = ( command , value ) = > command . propertyPath = value ,
["--value"] = ( command , value ) = > command . value = value ,
["--asset-path"] = ( command , value ) = > command . assetPath = value ,
["--parent-object-id"] = ( command , value ) = > command . parentObjectId = value ,
["--local-position"] = ( command , value ) = > command . localPosition = value ,
["--local-euler-angles"] = ( command , value ) = > command . localEulerAngles = value ,
["--local-scale"] = ( command , value ) = > command . localScale = value ,
["--view"] = ( command , value ) = > command . viewKind = value ,
["--output"] = ( command , value ) = > command . outputPath = value ,
["--width"] = ( command , value ) = > command . width = Integer ( "--width" , value ) ,
A4: admission policy and the editor lifecycle actions
BrokkrCommandPolicy (Runtime, pure, tested outside Unity) is consulted by
BrokkrEditorService before every BrokkrUnityCommandExecutor.Execute; it is the
only call path into the executor. The per-project allowed-action list defaults
to scene mutation plus lifecycle; createScriptableObject, createPrefabVariant
and saveScene are shipped denied (parked Q4 = A), and an action the list does
not name is denied. A denial gets a receipt with status denied and a reason.
The window's buttons write intents like any other caller, so operator and
agent share the gate.
Re-landed from the donor as fresh code: refreshAssets, setEditorPlayState,
setEditorPaused, captureEditorView, and the play-state fields on the host
snapshot (keys 14-17). The donor's captureEditorView wrote to any rooted
outputPath; it now resolves the path through one function that accepts only a
plain relative .png path under <project>/.brokkr/captures and refuses drive
letters, rooted or UNC paths, stream suffixes and any .. segment on every host
OS, and dimensions are bounded to 8192. requestedBy joins the intent (key 17)
and is echoed on the receipt (key 6). saveScene is new.
The window gains an Allowed Actions field bound to a per-project preference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:44:31 +02:00
["--height"] = ( command , value ) = > command . height = Integer ( "--height" , value ) ,
["--requested-by"] = ( command , value ) = > command . requestedBy = value
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
} ;
internal static async Task < int > RunAsync ( string [ ] args )
{
var options = Parse ( args ) ;
var cachePath = Path . GetFullPath ( Required ( options , "--unity-cache" ) ) ;
var action = Required ( options , "--action" ) ;
var readHost = string . Equals ( action , "readHost" , StringComparison . Ordinal ) ;
2026-09-29 16:16:50 +02:00
using var cache = await OpenAsync ( cachePath , readHost ) ;
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
return readHost ? await ReadHostAsync ( cache ) : await WriteIntentAsync ( cache , action , options ) ;
}
2026-09-29 16:16:50 +02:00
// An empty file, a legacy single-file store or a damaged shard fails inside CultCache with whatever exception the
// decoder throws; the caller gets one message that names the path.
private static async Task < CultCache > OpenAsync ( string cachePath , bool readOnly )
{
try
{
return await CultCacheMessagePack . OpenAsync ( cachePath , new CultCacheOpenOptions
{
UseDirectoryStore = true ,
ReadOnly = readOnly
} ) ;
}
catch ( Exception error ) when ( error is not ArgumentException )
{
throw new InvalidOperationException (
2026-09-29 16:25:15 +02:00
$"Brokkr store '{cachePath}' cannot be opened ({error.GetType().Name}: {error.Message}). An empty or legacy single-file store, or one another process holds, fails here; it must be a directory store." ,
2026-09-29 16:16:50 +02:00
error ) ;
}
}
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
private static async Task < int > ReadHostAsync ( CultCache cache )
{
await cache . PullAllBackingStoresAsync ( ) ;
var host = ReadHostSnapshot ( cache )
? ? throw new InvalidOperationException ( "Brokkr host snapshot 'unity/host/current' is unavailable." ) ;
Console . WriteLine ( $"project: {host.projectPath}" ) ;
Console . WriteLine ( $"observed: {host.observedAt}" ) ;
Console . WriteLine ( $"scene: {host.activeScenePath}" ) ;
Console . WriteLine ( $"agent-commands: {(host.agentCommandsEnabled ? " on " : " off ")}" ) ;
A4: admission policy and the editor lifecycle actions
BrokkrCommandPolicy (Runtime, pure, tested outside Unity) is consulted by
BrokkrEditorService before every BrokkrUnityCommandExecutor.Execute; it is the
only call path into the executor. The per-project allowed-action list defaults
to scene mutation plus lifecycle; createScriptableObject, createPrefabVariant
and saveScene are shipped denied (parked Q4 = A), and an action the list does
not name is denied. A denial gets a receipt with status denied and a reason.
The window's buttons write intents like any other caller, so operator and
agent share the gate.
Re-landed from the donor as fresh code: refreshAssets, setEditorPlayState,
setEditorPaused, captureEditorView, and the play-state fields on the host
snapshot (keys 14-17). The donor's captureEditorView wrote to any rooted
outputPath; it now resolves the path through one function that accepts only a
plain relative .png path under <project>/.brokkr/captures and refuses drive
letters, rooted or UNC paths, stream suffixes and any .. segment on every host
OS, and dimensions are bounded to 8192. requestedBy joins the intent (key 17)
and is echoed on the receipt (key 6). saveScene is new.
The window gains an Allowed Actions field bound to a per-project preference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:44:31 +02:00
Console . WriteLine ( $"state: playing={host.isPlaying} paused={host.isPaused} compiling={host.isCompiling} updating={host.isUpdating}" ) ;
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
Console . WriteLine ( $"objects: {host.sceneObjects.Length}" ) ;
foreach ( var sceneObject in host . sceneObjects . OrderBy ( item = > item . path , StringComparer . Ordinal ) )
{
var components = string . Join ( ", " , sceneObject . components . Select ( component = > component . typeName ) ) ;
Console . WriteLine ( $"- {sceneObject.path} active={sceneObject.activeSelf} layer={sceneObject.layer} components=[{components}]" ) ;
foreach ( var component in sceneObject . components )
foreach ( var property in component . properties . Where ( property = >
! property . path . StartsWith ( "m_" , StringComparison . Ordinal ) & &
! string . IsNullOrWhiteSpace ( property . value ) ) )
Console . WriteLine ( $" {component.typeName}.{property.path}={property.value}" ) ;
}
return 0 ;
}
private static async Task < int > WriteIntentAsync ( CultCache cache , string action , Dictionary < string , string > options )
{
Soul pass 3: conditional enable in Runtime, receipt floor, one write path
- The enable commit is conditional (ExpectUnchanged) and lives in the new
Runtime BrokkrCommandStore, with the pull-recompute-retry loop in
BrokkrCommandDrain.EnableAsync, so the tests exercise the shipped code. An
intent landing between the pull and the commit is expired, not run; eight
refusals in a row fail the enable loudly with the sink off. The probe is now
a thin fault-injecting wrapper over the real store, not a copy of it.
- Every editor write to the command lane is one cache commit; durable shard
logs are off (no server, no reader).
- No receipt is pruned before ten minutes, whatever the count, and the CLI
refuses a wait over five minutes. Fixed the comments that claimed otherwise.
- Pending and Prunable count a document only under the key its own id names.
- README and snapshot comment: the token stops stale EditorPrefs, it does not
authenticate writers; a restored or sync-conflict copy of the editor's own
store can rerun answered intents.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:52:34 +02:00
var waitMilliseconds = options . TryGetValue ( "--wait-ms" , out var wait ) ? Integer ( "--wait-ms" , wait ) : 10000 ;
// The editor keeps every receipt for at least BrokkrCommandDrain.ReceiptFloor, which is what makes a wait this
// long safe: the receipt a caller waits on cannot be pruned before it reads it.
if ( waitMilliseconds > BrokkrCommandDrain . MaxCallerWait . TotalMilliseconds )
throw new ArgumentException ( $"--wait-ms may not exceed {BrokkrCommandDrain.MaxCallerWait.TotalMilliseconds:F0} (the editor keeps receipts for at least {BrokkrCommandDrain.ReceiptFloor.TotalMinutes:F0} minutes)." ) ;
2026-09-29 16:16:50 +02:00
// A command id is single-use, so every invocation is a new act with a new id. --command-id exists for callers
// that need to name their own; one that already has a receipt is refused, because that receipt answered an
Soul pass 3: conditional enable in Runtime, receipt floor, one write path
- The enable commit is conditional (ExpectUnchanged) and lives in the new
Runtime BrokkrCommandStore, with the pull-recompute-retry loop in
BrokkrCommandDrain.EnableAsync, so the tests exercise the shipped code. An
intent landing between the pull and the commit is expired, not run; eight
refusals in a row fail the enable loudly with the sink off. The probe is now
a thin fault-injecting wrapper over the real store, not a copy of it.
- Every editor write to the command lane is one cache commit; durable shard
logs are off (no server, no reader).
- No receipt is pruned before ten minutes, whatever the count, and the CLI
refuses a wait over five minutes. Fixed the comments that claimed otherwise.
- Pending and Prunable count a document only under the key its own id names.
- README and snapshot comment: the token stops stale EditorPrefs, it does not
authenticate writers; a restored or sync-conflict copy of the editor's own
store can rerun answered intents.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:52:34 +02:00
// earlier act and would be reported as the answer to this one. The check and the write are two steps, so two
// callers naming the same new id in the same instant can still collide; ids are minted, and that is a caller defect.
2026-09-29 16:16:50 +02:00
var named = options . TryGetValue ( "--command-id" , out var commandId ) & & ! string . IsNullOrWhiteSpace ( commandId ) ;
if ( named )
{
await cache . PullAllBackingStoresAsync ( ) ;
if ( cache . TryGet ( BrokkrCommandLedger . ReceiptKey ( commandId ! ) , out BrokkrUnityCommandReceipt ? existing ) & & existing ! = null )
throw new ArgumentException (
$"Command id '{commandId}' already has a receipt ({existing.status}). Command ids are single-use; omit --command-id to get a fresh one." ) ;
}
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
var command = new BrokkrUnityCommand
{
2026-09-29 16:16:50 +02:00
commandId = named ? commandId ! : Guid . NewGuid ( ) . ToString ( "N" ) ,
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
action = action
} ;
foreach ( var ( name , value ) in options )
if ( Fields . TryGetValue ( name , out var assign ) )
assign ( command , value ) ;
await cache . AddAsync ( command , new CultRecordHandle < BrokkrUnityCommand > ( BrokkrCommandLedger . CommandKey ( command . commandId ) ) ) ;
await cache . FlushAsync ( ) ;
Soul pass 3: conditional enable in Runtime, receipt floor, one write path
- The enable commit is conditional (ExpectUnchanged) and lives in the new
Runtime BrokkrCommandStore, with the pull-recompute-retry loop in
BrokkrCommandDrain.EnableAsync, so the tests exercise the shipped code. An
intent landing between the pull and the commit is expired, not run; eight
refusals in a row fail the enable loudly with the sink off. The probe is now
a thin fault-injecting wrapper over the real store, not a copy of it.
- Every editor write to the command lane is one cache commit; durable shard
logs are off (no server, no reader).
- No receipt is pruned before ten minutes, whatever the count, and the CLI
refuses a wait over five minutes. Fixed the comments that claimed otherwise.
- Pending and Prunable count a document only under the key its own id names.
- README and snapshot comment: the token stops stale EditorPrefs, it does not
authenticate writers; a restored or sync-conflict copy of the editor's own
store can rerun answered intents.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:52:34 +02:00
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
if ( waitMilliseconds < = 0 )
{
Console . WriteLine ( command . commandId ) ;
return 0 ;
}
var receiptKey = BrokkrCommandLedger . ReceiptKey ( command . commandId ) ;
var deadline = DateTimeOffset . UtcNow . AddMilliseconds ( waitMilliseconds ) ;
2026-09-29 16:16:50 +02:00
var started = false ;
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
while ( DateTimeOffset . UtcNow < deadline )
{
await Task . Delay ( 100 ) ;
await cache . PullAllBackingStoresAsync ( ) ;
if ( ! cache . TryGet ( receiptKey , out BrokkrUnityCommandReceipt ? receipt ) | | receipt = = null )
continue ;
2026-09-29 16:16:50 +02:00
// Attempted is the editor's marker that it has started; the verdict is still to come.
if ( string . Equals ( receipt . status , BrokkrCommandDrain . AttemptedStatus , StringComparison . Ordinal ) )
{
started = true ;
continue ;
}
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
Console . WriteLine ( $"{receipt.status}: {receipt.message}" ) ;
if ( ! string . IsNullOrWhiteSpace ( receipt . objectId ) )
Console . WriteLine ( receipt . objectId ) ;
// The exit code is the receipt's verdict, never the write's success.
return string . Equals ( receipt . status , "accepted" , StringComparison . Ordinal ) ? 0 : 1 ;
}
var host = ReadHostSnapshot ( cache ) ;
var why = host is { agentCommandsEnabled : false }
? " The editor reports agent commands off; the operator enables them in the Brokkr window."
: "" ;
2026-09-29 16:16:50 +02:00
if ( started )
why = " The editor started this intent and has not recorded a result; it will not run it again." ;
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
throw new TimeoutException (
Decide "before the enable" by the store, batch the expiry, bound receipts
Soul pass 2 findings S1, S2, S3, S4.
- Enabling the sink is the drain's EnableAsync: every intent already in the
store gets an "expired" receipt and the enable marker is written, all in one
flush. No decision compares StoredAt with a clock any more, so a fast
writer clock and the Rust daemon's secs.nanosZ stamps stop mattering.
- The operator's EditorPrefs hold a token, and the sink runs only while the
store carries the marker with that token. A re-clone or a copied .brokkr has
no such marker, so a stale "enabled" authorizes nothing and the editor drops
it. The epoch and agentCommandsEnabledAt (host snapshot key 19) are gone.
- Selection is one pass over the store per tick (Pending), not a copy and sort
per intent.
- Receipts are capped at the newest 1000, pruned 128 pairs per tick with the
intent deleted before its receipt.
- A marker that could not be written is withdrawn, so the intent reads as
never started and is retried, not "interrupted".
- The drain writes through IBrokkrCommandStore; puts stage and one flush makes
them durable.
- The CLI timeout text no longer says the intent "stays written" without the
expiry caveat; the check-then-write window on --command-id is documented.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:56:33 +02:00
$"Timed out waiting for Brokkr receipt '{receiptKey.Value}'; the intent is still in the store: a running editor with the sink on will run it, but if the operator enables the sink after now it expires unrun.{why}" ) ;
A3: brokkr-command, the caller CLI, with every intent field and a real CultLib root
Re-landed from the donor as a fresh file (read with git show, not merged).
Changes from the donor: the CultLibRoot default is ..\..\CultLib instead of a
directory that does not exist; every field of brokkr.unity.command_intent.v0
has a --kebab option (the donor mapped six of seventeen), and unknown options
are refused before anything is written; keys come from BrokkrCommandLedger;
exit is 0 accepted, 1 other receipt, 2 usage error or timeout; readHost opens
the store read-only and prints the agent-commands flag; a timeout says when
the editor reports the sink off. The intent gains the capture fields at keys
13-16, which the CLI needs and A4's executor will read.
Tests run the built CLI as a process against the editor probe: field mapping,
fresh id per invocation, exit code from the receipt, timeout leaves only the
intent, unknown option writes nothing, and every action name reaches the
editor once with one receipt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:26:23 +02:00
}
private static BrokkrHostSnapshot ? ReadHostSnapshot ( CultCache cache ) = >
cache . TryGet ( new CultRecordKey ( "unity/host/current" ) , out BrokkrHostSnapshot ? host ) ? host : null ;
private static Dictionary < string , string > Parse ( string [ ] args )
{
var values = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
for ( var index = 0 ; index < args . Length ; index + = 2 )
{
if ( index + 1 > = args . Length | | ! args [ index ] . StartsWith ( "--" , StringComparison . Ordinal ) )
throw new ArgumentException ( "Arguments must be --name value pairs." ) ;
var name = args [ index ] ;
if ( name is not ( "--unity-cache" or "--action" or "--command-id" or "--wait-ms" ) & & ! Fields . ContainsKey ( name ) )
throw new ArgumentException ( $"Unknown option {name}." ) ;
values [ name ] = args [ index + 1 ] ;
}
return values ;
}
private static string Required ( Dictionary < string , string > options , string name ) = >
options . TryGetValue ( name , out var value ) & & ! string . IsNullOrWhiteSpace ( value )
? value
: throw new ArgumentException ( $"{name} is required." ) ;
private static int Integer ( string name , string value ) = >
int . TryParse ( value , NumberStyles . Integer , CultureInfo . InvariantCulture , out var parsed )
? parsed
: throw new ArgumentException ( $"{name} must be an integer." ) ;
}