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
64 changes: 64 additions & 0 deletions docs/reference/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,67 @@ with `wrighty actions` before deciding what is currently available.

See [worker lifecycle](worker.md), [claims and ownership](claims.md), and
[operator actions by surface](operator-actions.md) for the underlying procedures.

## Batch workflow actions

```shell
wrighty batch preview queue --status "Todo" --json
wrighty batch preview queue --status "Todo" --field area=api --json
wrighty batch preview send-back --id local:12 --id local:19 --json
wrighty batch preview resume --status "In Progress" --json
wrighty batch show <preview-id> --json
wrighty batch execute <preview-id> --yes --json
```

Select explicit IDs (duplicates collapse to canonical IDs), or one configured workflow status
with optional exact-match `--field` filters. Filters use the same AND semantics as `list`.
These selections cannot be mixed. Only active, eligible Local Markdown items enter the frozen set.
Previews sort canonical IDs ordinally and freeze the first 100 eligible items; JSON reports
`selectedCount`, `eligibleCount`, the exact `candidates`, and `limited` so truncation is explicit.
Each candidate includes its title, reviewed state fingerprint, before state, and authorization
consequence. Previewing persists display data and hashes but does not mutate or claim an item.

A preview expires five minutes after creation. Execute requires `--yes` in both human and JSON
modes, authorizing only this preview's operation and candidates. It starts no worker. Execution
rechecks configuration before starting and between candidates, then executes each item sequentially
through the individual action service. New matches cannot join the set; changed content, claim,
session, or eligibility produces a skipped item. The reviewed-state fingerprint is stricter than
the web Board's fresh eligibility check, so an otherwise harmless content edit also requires a
new CLI preview. The CLI and Board use the same Core batch loop for sequencing, conflict classification,
cancellation, and partial results after a systemic failure. Limits, lifetime, action eligibility,
and backend execution are also shared. Each interface retains its own preview storage and
revalidation inputs.

All three commands return `schemaVersion: 1` and `result` containing `preview`, `state` (`preview`
or `completed`), `items`, `stopCode`, and `hasIssues`. Item outcomes are `applied`, `skipped`,
`failed`, or `unprocessed`. Applied items include their single-action before/after result.
A systemic failure stops remaining work; no successful mutation is rolled back. An ambiguous
failure has `mutationMayHaveApplied: true` and must be inspected before any retry. Execution exits
0 when all items applied, 6 for partial results, or 130 for cancellation; these outcomes retain
stdout JSON. Validation failures use normal stderr errors. `show` exits 0 for a readable record.

Preview and result journals are scoped by the absolute configuration path under
`<cache/state-root>/workflow-batches-v1/<configuration-hash>/`. Separate invocations using that
configuration and cache can share previews. An exclusive configuration-scoped file lock serializes
execution and inspection; contention returns `STORE_BUSY`. Completed records are returned on
repeat execution, even after preview expiry, without reapplying any item. Explicit `--yes` remains
required. A preview contains no claim credentials and is not execution authority.

Before each mutation, Wrighty writes an in-flight marker and flushes it to disk. If execution
stops or the host restarts, the next `show`/`execute` marks an unfinished run interrupted: the
in-flight item is failed with an uncertain outcome, and remaining items are unprocessed. Wrighty
does not resume an interrupted batch automatically. Cancellation between items preserves definite
outcomes; cancellation during a mutation is conservatively uncertain. If journal persistence or
stdout fails, inspect the existing batch before attempting another mutation.

The Board keeps previews/results in its web process and maps the shared executor's outcomes into
its warning panel. An accepted web batch continues if the browser disconnects. Unexpected backend
failures are retained as partial results and identify any item that may have been mutated, so a
repeated submission returns the result instead of replaying the batch. Stopping the web process
still loses its in-memory record; the CLI journal's restart recovery is specific to CLI batches.

Records untouched for 24 hours are removed when creating another preview; each configuration holds
at most 512 records. Cache deletion loses preview/result evidence and makes old IDs unavailable;
it does not undo item mutations. Never restore, copy, or edit journals to retry work. For an expired
or missing preview, inspect current items and obtain a newly reviewed selection. Follow up with
`workers --item <id> --json` when pickup assessment is needed.
5 changes: 5 additions & 0 deletions docs/reference/agent-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,8 @@ through `actions <id> <name> --exec --yes --expected-version <stateVersion> --js
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).

The skill also supports reviewed Local Markdown batches through `batch preview`, `batch show`,
and `batch execute --yes`. It reports the exact frozen subset and partial results, including
uncertain outcomes after interruption. CLI and web batch operations share the Core execution loop;
the CLI persists its own previews/results for cross-process use. See [batch workflow actions](actions.md#batch-workflow-actions).
1 change: 1 addition & 0 deletions docs/reference/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ runtime-specific filename placeholders rather than additional base directories.
| | `sessions-v1.json` | Legacy runtime state | GitHub | No | Pre-migration session records may be lost | [Legacy files](#legacy-files) |
| | `provider-availability-v1.json` | Legacy operational cache | Both | No | Pre-migration provider state may be lost | [Legacy files](#legacy-files) |
| | `*.tmp` | Atomic-write temporary files | Both | No | Normally safe only when no Wrighty process is running | [Installation state and cache](#installation-state-and-cache) |
| `<cache/state-root>/workflow-batches-v1/` | `<configuration-hash>/<preview-id>.json`, `.lock`, `*.tmp` | Machine-local execution journal | Local Markdown | No | Frozen previews and interruption/replay evidence are lost; mutations remain applied | [Batch workflow actions](actions.md#batch-workflow-actions) |
| `<cache/state-root>/worker-instances-v1/` | `<configuration-hash>/<run-id>.json` | Machine-local runtime state | Both | No | Worker liveness and configuration-drift observations disappear until processes register again | [Installation state and cache](#installation-state-and-cache) |
| | `<configuration-hash>/<run-id>.stop.json` | Temporary machine-local control request | Both | No | A pending cooperative drain/interrupt request is lost | [Installation state and cache](#installation-state-and-cache) |
| `<cache/state-root>/worker-interruptions-v1/` | `<run-id>-<item-hash>.json` | Temporary interruption-recovery breadcrumb | Both | No | An incomplete interrupted-run finalizer is harder to diagnose; item and claim state remain authoritative | [Installation state and cache](#installation-state-and-cache) |
Expand Down
4 changes: 2 additions & 2 deletions skills/wrighty/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ description: Safely operate Wrighty through the `wrighty` CLI. Use only when the

# Wrighty

<!-- wrighty-skill-version: 0.18.0 -->
<!-- wrighty-skill-version: 0.19.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, individual Queue/Send back/Resume actions, worker discovery, and pickup prospects, read
For board overview, triage, individual and batch 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: 34 additions & 0 deletions skills/wrighty/references/board-and-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,40 @@ but worker assessment failed: inspect again without replaying the mutation. If a
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.

## Batch workflow actions

For a requested Local Markdown batch, freeze the selection before authorizing execution:

```shell
wrighty batch preview queue --status "Todo" --json
wrighty batch preview send-back --id local:12 --id local:19 --json
wrighty batch show <preview-id> --json
wrighty batch execute <preview-id> --yes --json
```

Use configured statuses, or explicit IDs from the user's selection. A status selection can include
`--field name=value` filters with the same exact-match AND semantics as `list`. Use `resume` for
retained sessions. Review `result.preview.candidates`, each item's `consequence`, `selectedCount`,
`eligibleCount`, `limited`, and `expiresAt`. At most 100 eligible items are frozen for five minutes.
If limited, explain which exact subset will run; never silently process subsequent batches.

Pass `--yes` only when the user's authorization covers the frozen items and their consequences.
If that effect is already authorized, proceed without another confirmation. Execution starts no
worker and never claims newly eligible items outside the frozen selection. Each candidate's
reviewed state is revalidated under the same mutation lock as individual actions.

Read stdout JSON even on exit 6 (partial result) or 130 (cancellation). Report `result.items` as
applied, skipped, failed, or unprocessed; explain `code` and `mutationMayHaveApplied`. A systemic
failure stops the rest without rolling back applied items. Repeating execute returns the stored
result; it does not retry skipped, failed, or unprocessed items. After a process interruption,
`batch show` recovers the journal and flags an in-flight item as uncertain. Inspect that item's
current state before proposing a new preview. Do not recreate and replay the whole batch blindly.

A missing/expired preview requires a fresh review. Previews/results belong to the same local
configuration and cache directory across processes and restarts; do not copy or edit the files,
or treat a preview ID as authority to execute. Use `workers --item <id> --json` for pickup evidence
after a successful action when the user's request needs it.

## Workers and pickup prospects

Run `wrighty workers --json` for worker discovery alone. It reads the configuration-scoped local
Expand Down
1 change: 1 addition & 0 deletions src/Highbyte.Wrighty.Cli/CliApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ private RootCommand BuildRootCommand()
root.Subcommands.Add(BuildWorkersCommand());
root.Subcommands.Add(BuildGetCommand());
root.Subcommands.Add(BuildActionsCommand());
root.Subcommands.Add(BuildBatchCommand());
root.Subcommands.Add(BuildContextCommand());
root.Subcommands.Add(BuildApproveCommand());
root.Subcommands.Add(BuildApprovalWorkflowCommand());
Expand Down
106 changes: 106 additions & 0 deletions src/Highbyte.Wrighty.Cli/CliBatchCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.CommandLine;
using Highbyte.Wrighty.Actions;
using Highbyte.Wrighty.Configuration;
using Highbyte.Wrighty.Errors;
using Highbyte.Wrighty.Models;

namespace Highbyte.Wrighty.Cli;

public sealed partial class CliApplication
{
private Command BuildBatchCommand()
{
var command = new Command("batch", "Preview, execute, or inspect a frozen workflow batch");
command.Subcommands.Add(BuildBatchPreviewCommand());
command.Subcommands.Add(BuildBatchRecordCommand(execute: false));
command.Subcommands.Add(BuildBatchRecordCommand(execute: true));
return command;
}

private WorkflowBatchStore BatchStore => new(storageLocations.WorkflowBatchesRoot);

private Command BuildBatchPreviewCommand()
{
var action = new Argument<string>("action-name");
var ids = new Option<string[]>("--id") { Description = "Explicit item ID; repeat to select several items." };
var status = new Option<string?>("--status") { Description = "Select active items in this workflow status." };
var fields = FieldOption("Filter the status selection by name=value; repeat for AND semantics.");
var json = JsonOption();
var command = new Command("preview", "Freeze up to 100 eligible items for five minutes; no items are changed");
command.Arguments.Add(action);
command.Options.Add(ids);
command.Options.Add(status);
command.Options.Add(fields);
command.Options.Add(json);
command.SetAction((parsed, token) => ExecuteAsync(parsed.GetValue(json), async config =>
{
WorkflowActionService.EnsureSupported(parsed.GetValue(action)!);
var selection = await SelectBatchItemsAsync(config, parsed.GetValue(ids) ?? [],
parsed.GetValue(status), parsed.GetValue(fields) ?? [], token);
var preview = await new WorkflowBatchService(tracker, BatchStore).PreviewAsync(
config, parsed.GetValue(action)!, selection, token);
await writer.WriteBatchAsync(new(preview, "preview", []), parsed.GetValue(json));
}, token));
return command;
}

private async Task<IReadOnlyList<WorkItemId>> SelectBatchItemsAsync(TrackerConfig config,
string[] ids, string? status, string[] fields, CancellationToken cancellationToken)
{
if (tracker.Backend(config) is not IWorkflowActionBackend)
throw new TrackerException("NOT_SUPPORTED", "Batch workflow actions require Local Markdown.", 3);
if ((ids.Length == 0 && string.IsNullOrWhiteSpace(status)) ||
(ids.Length > 0 && (status is not null || fields.Length > 0)))
throw new TrackerException("ARGUMENT_INVALID", "Select --id values or --status with optional --field filters.", 2);
if (ids.Length > 0) return ids.Select(id => tracker.ResolveId(config, id)).ToArray();
var items = await tracker.ListAsync(config, new ListWorkItemsRequest(status, null, ArchiveScope.Active,
ParseFields(fields, allowDeletion: false).ToDictionary(pair => pair.Key, pair => pair.Value!, StringComparer.Ordinal)),
cancellationToken);
return items.Select(item => item.Id).ToArray();
}

private Command BuildBatchRecordCommand(bool execute)
{
var id = new Argument<string>("preview-id");
var yes = new Option<bool>("--yes") { Description = "Authorize exactly the persisted preview's items and action." };
var json = JsonOption();
var command = new Command(execute ? "execute" : "show", execute
? "Execute a reviewed preview once, or return its recorded result"
: "Inspect a preview or its recorded result without executing items");
command.Arguments.Add(id);
command.Options.Add(json);
if (execute) command.Options.Add(yes);
command.SetAction((parsed, token) => RunBatchRecordAsync(
new(parsed.GetValue(id)!, execute, parsed.GetValue(yes), parsed.GetValue(json)), token));
return command;
}

private sealed record BatchRecordRequest(string Id, bool Execute, bool Yes, bool Json);

private async Task<int> RunBatchRecordAsync(BatchRecordRequest request, CancellationToken token)
{
var resultExit = 0;
var exit = await ExecuteAsync(request.Json, async config =>
{
var store = BatchStore;
var record = await store.ReadAsync(config, request.Id, token);
if (request.Execute)
{
if (!request.Yes)
throw new TrackerException("BATCH_CONFIRMATION_REQUIRED",
"Review batch show output, then pass --yes to authorize exactly that preview.", 2);
record = await new WorkflowBatchService(tracker, store).ExecuteAsync(config,
request.Id, ct => configLoader.LoadAsync(workingDirectory, ct), token);
resultExit = BatchExitCode(record);
}
await writer.WriteBatchAsync(record, request.Json);
}, token);
return exit == 0 ? resultExit : exit;
}

private static int BatchExitCode(WorkflowBatchRecord record)
{
if (record.StopCode == "BATCH_CANCELLED") return 130;
return record.HasIssues ? 6 : 0;
}
}
31 changes: 31 additions & 0 deletions src/Highbyte.Wrighty.Cli/Output/BatchOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Highbyte.Wrighty.Actions;

namespace Highbyte.Wrighty.Cli.Output;

public sealed partial class OutputWriter
{
public async Task WriteBatchAsync(WorkflowBatchRecord record, bool json)
{
if (json)
{
await WriteJsonAsync(new { schemaVersion = 1, result = record });
return;
}
var preview = record.Preview;
await output.WriteLineAsync($"Batch {preview.Id}: {preview.Action}; {record.State}.");
await output.WriteLineAsync($"Selected {preview.SelectedCount}; eligible {preview.EligibleCount}; frozen {preview.Candidates.Count}. " +
$"Preview expires {preview.ExpiresAt:O}. No worker is started.");
foreach (var candidate in preview.Candidates)
{
await output.WriteLineAsync($" {candidate.Id}: {candidate.Title}");
await output.WriteLineAsync($" {candidate.Consequence}");
}
if (record.State == "preview")
await output.WriteLineAsync($"After review: wrighty batch execute {preview.Id} --yes");
foreach (var item in record.Items)
await output.WriteLineAsync($" {item.Id}: {item.Outcome}{(item.Code is null ? "" : $" ({item.Code})")}" +
(item.MutationMayHaveApplied ? "; mutation may have applied — inspect before retrying." : "."));
if (record.StopCode is not null)
await output.WriteLineAsync($"Stopped: {record.StopCode}. Remaining items were not processed.");
}
}
Loading