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
43 changes: 36 additions & 7 deletions docs/reference/actions.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Action discovery
# Action discovery and workflow execution

Use `wrighty actions` to inspect what you can do with an item before choosing an operation:

Expand All @@ -15,10 +15,39 @@ reason codes. An optional action name selects one descriptor; an unknown name re
Wrighty's normal nonzero exit status and stderr JSON contract. Do not combine a selected name with
`--all`.

Discovery is read-only. All descriptors currently report `execution: "manual-only"`, and `--exec`
returns `ACTION_EXECUTION_UNSUPPORTED`. Review the displayed guidance and use the existing focused
CLI command or web control when you have authorized the operation. Listing an action never claims
an item, starts a vendor session, grants permission, or overrides a pending retry.
Discovery is read-only. Queue, Send back, and Resume report `execution: "supported"`; other
catalogue entries remain `manual-only`. Listing an action never claims an item, starts a vendor
session, grants permission, or overrides a pending retry.

## Execute one workflow action

```shell
wrighty actions local:42 queue --json
wrighty actions local:42 queue --exec --yes --expected-version <stateVersion> --json
wrighty actions local:42 send-back --exec --yes --json
wrighty actions local:42 resume --exec --yes --json
```

These three executors support Local Markdown and share the web Board's policy and backend
operation. `--exec` requires one action name and cannot use `--all`. An interactive invocation
shows the consequence and prompts; redirected input and JSON require `--yes`. This authorizes
only the named operation. Neither discovery nor `--yes` grants takeover or starts a worker.

Supply the discovery's `stateVersion` with `--expected-version` when executing a reviewed
snapshot. Wrighty always reads current state again, then validates and mutates under the local
store lock. A changed item, claim, session, or configuration refuses the old version with
`ACTION_STATE_CHANGED` (or the current action's more specific refusal). The fingerprint is not a
reservation or a credential. Without it, execution uses a fresh observation from this invocation.
Manual-only actions still return `ACTION_EXECUTION_UNSUPPORTED`; commands, URLs, and item text
are never interpreted as executors or shell input.

Execution JSON has `schemaVersion: 1` and a `result` with `itemId`, `action`, `outcome: "applied"`,
`observedAt`, `stateVersion`, `before`, `after`, and `startsWorker: false`. Each state contains the
workflow status, operational status, execution authorization, and dispatch marker. `workers`
contains a fresh pickup assessment after mutation. If that follow-up fails, `refreshError` is
`WORKER_REFRESH_UNAVAILABLE` and the result still says applied; inspect before attempting another
mutation. A worker may claim the item immediately afterward, so even a successful result remains
an observation rather than a reservation.

## Action vocabulary

Expand All @@ -37,7 +66,7 @@ an item, starts a vendor session, grants permission, or overrides a pending retr
| `inspect-recovery` | Read the item and operational status for current recovery details. |

Names are stable selectors; titles and descriptions are presentation. Queue, Send back, and Resume
currently describe the corresponding Board controls; they do not have generic CLI executors.
share the corresponding Board controls' eligibility and execution path.
With worker-queue authorization enabled, Queue authorizes automatic processing and Send back revokes
that authorization. When it is disabled, execution policy remains independent. Resume queues the
recorded session and does not start a worker. These actions are not interchangeable status moves.
Expand All @@ -50,7 +79,7 @@ process immediately. Recommendations never authorize execution.

`--json` returns `schemaVersion: 1` and `result` containing:

- `itemId`, `stateObservedAt`, and nullable `recommendedAction`;
- `itemId`, `stateObservedAt`, `stateVersion`, and nullable `recommendedAction`;
- `actions[]` with `name`, `title`, `description`, and `recommended`;
- `availability`, `unavailableCode`, and `unavailableReason`;
- `kind`, `execution`, `confirmation`, `requiresTty`, and `startsProcess`; and
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/agent-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,9 @@ shared action discovery, and scoped worker/pickup assessment. `list --json` keep
and adds `listing` metadata: status order, archive/filter/limit scope, returned count, and whether
the result may be truncated. See [worker discovery](workers.md). Update an installed skill through
the existing `wrighty skill update` command to receive these workflows.

The bundled skill also executes individual Local Markdown Queue, Send back, and Resume requests
through `actions <id> <name> --exec --yes --expected-version <stateVersion> --json`, after the
requested effect is authorized. CLI and web share eligibility and locked revalidation. Execution
returns the resulting state and refreshed worker pickup evidence; it does not launch a worker.
See [action execution](actions.md).
6 changes: 3 additions & 3 deletions skills/wrighty/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
---
name: wrighty
description: Safely operate Wrighty through the `wrighty` CLI. Use only when the user explicitly asks to use Wrighty, the Wrighty CLI, or a Wrighty work-item ID, including board overview, worker discovery, pickup assessment, and requests to list, inspect, create, pick, claim, edit, move, finish, archive, or release tracked work. Do not trigger for generic tasks, GitHub issues, planning, backlogs, or project management that do not explicitly identify Wrighty.
description: Safely operate Wrighty through the `wrighty` CLI. Use only when the user explicitly asks to use Wrighty, the Wrighty CLI, or a Wrighty work-item ID, including board overview, worker discovery, pickup assessment, Queue/Send back/Resume actions, and requests to list, inspect, create, pick, claim, edit, move, finish, archive, or release tracked work. Do not trigger for generic tasks, GitHub issues, planning, backlogs, or project management that do not explicitly identify Wrighty.
---

# Wrighty

<!-- wrighty-skill-version: 0.17.0 -->
<!-- wrighty-skill-version: 0.18.0 -->

Operate Wrighty state only through the `wrighty` command. Never mutate tracked state by editing
local Markdown, invoking `gh`, calling GitHub APIs/MCP, writing claim comments, or changing Project
fields directly.

For board overview, triage, available actions, worker discovery, and item pickup prospects, read
For board overview, triage, individual Queue/Send back/Resume actions, worker discovery, and pickup prospects, read
[references/board-and-workers.md](references/board-and-workers.md). Use `list --json` for workflow
columns, `actions <id> --json` for the shared action catalogue, and `workers [--item <id>] --json`
for scoped worker evidence. Discovery does not authorize mutations or worker launches.
Expand Down
34 changes: 30 additions & 4 deletions skills/wrighty/references/board-and-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,36 @@ Retry-scheduled and handoff-queued work is deferred; do not classify it as await

Use `wrighty actions <id> --json` for available actions; add `--all` when the user asks why an
alternative is unavailable. Use action names, reasons, recommendation, and execution metadata
from the response. A recommendation is advice, not execution authority. The current catalogue is
manual-only: `actions --exec` refuses execution. Queue/Send back/Resume descriptors currently
refer to the matching web Board operations; do not substitute a generic move or direct Markdown
edit to imitate an unavailable typed operation.
from the response. A recommendation is advice, not execution authority.

## Individual workflow actions

For an authorized Queue, Send back, or Resume request, inspect the selected action and its
consequence with `wrighty actions <id> <name> --json`. On Local Markdown, these actions report
`execution: "supported"`. Explain any automatic-processing consequence if the user's request has
not already authorized it; do not add another confirmation once that exact effect is authorized.
Then execute the stable name with the returned `result.stateVersion`:

```shell
wrighty actions <id> queue --exec --yes --expected-version <stateVersion> --json
```

Use `send-back` or `resume` for those intents. Queue authorizes automatic processing when the
worker-queue policy is enabled; Send back revokes that authorization. With that policy disabled,
execution authorization stays independent. Resume queues the recorded session and preserves the
requirements, context, and execution selection; it does not start a worker. Interactive
`resume-session` is a different action and remains manual-only in this catalogue.

The command revalidates current state under the backend's mutation lock. On `ACTION_STATE_CHANGED`,
claim contention, missing session, or backend refusal, inspect again and report the specific reason.
Do not force takeover, substitute another action, or imitate these operations with a generic
status move or direct Markdown edit. These executors currently support Local Markdown only.

Read `result.outcome`, `before`, and `after` to report the applied transition, then use `workers`
for refreshed pickup prospects. An applied result with `refreshError` means the mutation succeeded
but worker assessment failed: inspect again without replaying the mutation. If a command fails
without a definitive outcome, re-read the item before any retry. Other catalogue entries remain
manual-only; use their documented focused procedure only within the user's authorization.

## Workers and pickup prospects

Expand Down
86 changes: 73 additions & 13 deletions src/Highbyte.Wrighty.Cli/CliActionCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,87 @@ private Command BuildActionsCommand()
var name = new Argument<string?>("action-name") { Arity = ArgumentArity.ZeroOrOne };
var all = new Option<bool>("--all") { Description = "Include unavailable actions and their reasons." };
var json = JsonOption();
var exec = new Option<bool>("--exec") { Description = "Reserved; action execution is not supported yet." };
var command = new Command("actions", "Discover the actions available for a work item (read-only)");
var exec = new Option<bool>("--exec") { Description = "Execute one supported workflow action after revalidation." };
var yes = new Option<bool>("--yes") { Description = "Authorize the selected action without prompting." };
var expected = new Option<string?>("--expected-version") { Description = "Require the state version returned by action discovery." };
var command = new Command("actions", "Discover or explicitly execute a work item action");
command.Arguments.Add(id);
command.Arguments.Add(name);
command.Options.Add(all);
command.Options.Add(json);
command.Options.Add(exec);
command.SetAction((parsed, cancellationToken) => ExecuteAsync(parsed.GetValue(json), async config =>
{
if (parsed.GetValue(exec))
throw new TrackerException("ACTION_EXECUTION_UNSUPPORTED",
"Action discovery is read-only; use the documented focused command after review.", 2);
var itemId = tracker.ResolveId(config, parsed.GetValue(id)!);
var state = await tracker.GetOperationalAsync(config, itemId, cancellationToken);
var discovery = await DiscoverActionsAsync(config, state, cancellationToken);
var shown = SelectActions(discovery, parsed.GetValue(name), parsed.GetValue(all));
await writer.WriteActionsAsync(shown, parsed.GetValue(json));
}, cancellationToken));
command.Options.Add(yes);
command.Options.Add(expected);
command.SetAction((parsed, cancellationToken) => ExecuteAsync(parsed.GetValue(json),
config => RunActionCommandAsync(config, new ActionCommandRequest(
parsed.GetValue(id)!, parsed.GetValue(name), parsed.GetValue(all), parsed.GetValue(exec),
parsed.GetValue(yes), parsed.GetValue(expected), parsed.GetValue(json)),
cancellationToken), cancellationToken));
return command;
}

private sealed record ActionCommandRequest(
string Id, string? Name, bool All, bool Execute, bool Yes, string? ExpectedVersion, bool Json);

private async Task RunActionCommandAsync(TrackerConfig config, ActionCommandRequest request,
CancellationToken cancellationToken)
{
var (id, selected, all, execute, yes, expectedVersion, json) = request;
ValidateActionOptions(selected, all, execute, yes, expectedVersion);
var itemId = tracker.ResolveId(config, id);
var state = await tracker.GetOperationalAsync(config, itemId, cancellationToken);
var discovery = await DiscoverActionsAsync(config, state, cancellationToken);
var shown = SelectActions(discovery, selected, all);
if (!execute)
{
await writer.WriteActionsAsync(shown, json);
return;
}
await ConfirmWorkflowActionAsync(shown.Actions.Single(), yes, json, cancellationToken);
config = await configLoader.LoadAsync(workingDirectory, cancellationToken);
var result = await new WorkflowActionService(tracker).ExecuteAsync(config, itemId, selected!,
expectedVersion ?? discovery.StateVersion, cancellationToken);
await WriteExecutedActionAsync(config, result, json, cancellationToken);
}

private static void ValidateActionOptions(string? selected, bool all, bool execute, bool yes, string? expectedVersion)
{
if (execute && (selected is null || all))
throw new TrackerException("ARGUMENT_INVALID", "--exec requires one action name and cannot use --all.", 2);
if (!execute && (yes || expectedVersion is not null))
throw new TrackerException("ARGUMENT_INVALID", "--yes and --expected-version require --exec.", 2);
if (execute) WorkflowActionService.EnsureSupported(selected!);
}

private async Task WriteExecutedActionAsync(TrackerConfig config, WorkflowActionResult result, bool json,
CancellationToken cancellationToken)
{
WorkerDiscovery? workers = null;
string? refreshError = null;
try { workers = await ReadWorkersAsync(config, result.ItemId, cancellationToken); }
catch (Exception exception) when (exception is TrackerException or IOException or UnauthorizedAccessException or OperationCanceledException)
{
// The mutation succeeded. Failed follow-up inspection must not invite replay.
refreshError = "WORKER_REFRESH_UNAVAILABLE";
}
await writer.WriteWorkflowActionAsync(result, workers, refreshError, json);
}

private async Task ConfirmWorkflowActionAsync(OperationalAction action, bool yes, bool json,
CancellationToken cancellationToken)
{
if (yes) return;
if (json || isInputRedirected())
throw new TrackerException("ACTION_CONFIRMATION_REQUIRED",
$"{action.Description} Pass --yes to authorize this operation.", 2);
await output.WriteLineAsync(action.Description);
await output.WriteAsync($"Confirm {action.Title}? [y/N] ");
var answer = await input.ReadLineAsync(cancellationToken);
if (!string.Equals(answer, "y", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(answer, "yes", StringComparison.OrdinalIgnoreCase))
throw new TrackerException("ACTION_CONFIRMATION_REQUIRED", "The action was cancelled.", 2);
}

private static OperationalActionDiscovery SelectActions(
OperationalActionDiscovery discovery, string? selected, bool all)
{
Expand Down
8 changes: 6 additions & 2 deletions src/Highbyte.Wrighty.Cli/CliWorkerDiscoveryCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ private Command BuildWorkersCommand()

private async Task InspectWorkersAsync(TrackerConfig config, string? item, bool json,
CancellationToken cancellationToken)
=> await writer.WriteWorkersAsync(await ReadWorkersAsync(config, item, cancellationToken), json);

private async Task<WorkerDiscovery> ReadWorkersAsync(TrackerConfig config, string? item,
CancellationToken cancellationToken)
{
var configurationPath = config.SourcePath ?? Path.Combine(workingDirectory, TrackerConfigLoader.FileName);
var snapshot = await workerInstances.InspectAsync(configurationPath, cancellationToken);
Expand All @@ -39,7 +43,7 @@ private async Task InspectWorkersAsync(TrackerConfig config, string? item, bool
snapshot.ObservedAt, cancellationToken);
workers.Add(WorkerDiscoveryEntry.From(status, revision, pickup));
}
await writer.WriteWorkersAsync(new(snapshot.ObservedAt, snapshot.ConfigurationPathHash,
snapshot.Coverage, snapshot.Detail, revision, state?.Item.Id.Value, workers), json);
return new(snapshot.ObservedAt, snapshot.ConfigurationPathHash,
snapshot.Coverage, snapshot.Detail, revision, state?.Item.Id.Value, workers);
}
}
17 changes: 17 additions & 0 deletions src/Highbyte.Wrighty.Cli/Output/ActionOutput.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Highbyte.Wrighty.Actions;
using Highbyte.Wrighty.Workers;

namespace Highbyte.Wrighty.Cli.Output;

Expand All @@ -19,6 +20,22 @@ public async Task WriteActionsAsync(OperationalActionDiscovery discovery, bool j
await WriteActionAsync(action);
}

public async Task WriteWorkflowActionAsync(WorkflowActionResult result, WorkerDiscovery? workers,
string? refreshError, bool json)
{
if (json)
{
await WriteJsonAsync(new { schemaVersion = 1, result, workers, refreshError });
return;
}
await output.WriteLineAsync($"{result.Action} applied to {result.ItemId}: " +
$"{result.Before.Status} → {result.After.Status}; {result.After.OperationalStatus}.");
await output.WriteLineAsync($"Automatic execution: {result.After.AutomaticExecutionAllowed}. No worker started.");
if (workers is not null) await WriteWorkersAsync(workers, false);
if (refreshError is not null)
await output.WriteLineAsync("Action applied; worker assessment could not be refreshed. Inspect before retrying.");
}

private async Task WriteActionAsync(OperationalAction action)
{
await output.WriteLineAsync();
Expand Down
6 changes: 3 additions & 3 deletions src/Highbyte.Wrighty.Core/Actions/OperationalAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ public sealed record OperationalAction(
string? AgentPrompt = null,
bool Recommended = false)
{
// The foundation deliberately has no executors, including for otherwise available actions.
public string Execution { get; } = "manual-only";
public string Execution { get; init; } = "manual-only";

public static OperationalAction FromGuidance(
WorkerOperatorAction guidance,
Expand Down Expand Up @@ -48,4 +47,5 @@ public sealed record OperationalActionDiscovery(
string ItemId,
DateTimeOffset StateObservedAt,
string? RecommendedAction,
IReadOnlyList<OperationalAction> Actions);
IReadOnlyList<OperationalAction> Actions,
string? StateVersion = null);
Loading