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
2 changes: 2 additions & 0 deletions docs/reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ first-run guide. These pages are the authority for options, guarantees, and edge

Related: [workflow guide](../workflows.md), [item metadata](../item-metadata/README.md), and
[design documents](../design/).

- [Worker discovery](workers.md): scoped run inspection and item pickup assessment.
6 changes: 6 additions & 0 deletions docs/reference/agent-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,9 @@ only for unresolved decisions that materially affect the result. Explicit tracke
incomplete, but the skill does not present them as ready or enable automatic processing until the
same assessment passes. Fresh worker sessions independently assess the approved context they
receive; the skill does not stamp items with a reusable “verified” marker.

The bundled skill supports board overview using configured workflow order, operational triage,
shared action discovery, and scoped worker/pickup assessment. `list --json` keeps its `result` array
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.
3 changes: 3 additions & 0 deletions docs/reference/worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -1436,3 +1436,6 @@ target agent is told why it has no history instead of silently assuming there wa

These CLI surfaces are version-sensitive. Validate vendor upgrades in a throwaway repository before
unattended use.

Use [`wrighty workers`](workers.md) for focused local run discovery or `workers --item <id> --json`
for advisory pickup prospects. Inspection alone does not start a worker.
70 changes: 70 additions & 0 deletions docs/reference/workers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Worker discovery

`wrighty workers` lists registered worker runs in the current local configuration scope. Plain
listing reads configuration and registry files and probes only registered PIDs and their start
identities. It does not contact the tracker, inspect retained worktrees, start an agent, or clean
up expired registry records.

```sh
wrighty workers --json
wrighty workers --item local:42 --json
```

`--item` resolves the canonical item ID and adds advisory pickup assessment using tracker state
and the same selection/admission primitives as the worker. It can read local runtime, context,
provider-cache, and workspace evidence, but does not claim work, authorize the queue, prepare a
workspace, or run a paid provider probe. Execution still revalidates and atomically claims work.

## JSON contract

The versioned envelope has `schemaVersion: 1` and a `result` with:

| Field | Meaning |
| --- | --- |
| `observedAt` | Start of this observation; the snapshot is not a reservation. |
| `scope`, `configurationPathHash` | Registered workers in this installation/configuration. |
| `coverage`, `detail` | `complete`, `incomplete`, or `unavailable` registry coverage and any explanation. |
| `configurationRevision` | Current repository configuration revision, when readable. |
| `itemId` | Canonical assessed item, when requested. |
| `localWorkers` | Individual run entries; an empty array does not prove global absence. |

Each entry reuses the `instance` and `detail` projection from `status --json` and adds named
`liveness` (`Running`, `Stale`, `Unknown`), `origin` (`cli-process`, `web-hosted`, `unknown`),
`reportedState`, `intake`, `remainingItemAllowance`, `idleExpiresAt`, `configurationDrift`, and
optional `pickup`. The nested instance retains its existing enum representation; `status --json`
is unchanged apart from the additive scheduling/progress fields.

`instance` includes run ID, PID/start identity, CLI/web-host origin, last heartbeat/state/item/agent,
startup configuration revision, and cooperative control capabilities. Several web-hosted runs may
share a PID; address a run by its run ID, never by PID alone.

New registrations include `scheduling`: continuous/bounded/targeted mode, canonical target and
intent, effective source/active statuses, explicit/default agent, filters, workspace mode/repository,
item limit, idle/item timeout, profile, and dry-run mode. These values are captured at startup;
`invocationSummary` remains display text. No claim token is included. `progress` reports completed
item accounting and the current idle-period start from the worker loop. Missing legacy fields mean
unknown, not unrestricted eligibility. Remaining allowance excludes an active item; no configured
limit means unlimited, while missing progress means unknown.

A `pickup` includes outcome (`could-pick-up`, `cannot-pick-up`, `unknown`), a stable reason code,
explanation, resolved agent when known, and flags for already processing or eligibility after the
current item. It considers intake/lifetime, target/limits, filters, workflow and execution policy,
claims, recorded sessions/dispatch, runtime/agent enablement, approved context, cached provider
state, workspace evidence, and configuration drift. Inconclusive checks yield unknown.

A due retry remains subject to retained-session rules; discovery never bypasses its timer.
Already-processing reports the worker's last-reported activity and does not authorize competing
work. A possible pickup is not a promise about queue order or timing. Workspace and provider state
can change immediately afterwards. Reassess after queueing or resuming; this command does not
predict a proposed mutation's consequences.

## Liveness and coverage

Records use cached heartbeat metadata plus targeted OS process/start-identity checks. A missing
process, reused PID, or expired heartbeat is stale; denied identity inspection is unknown. Older
workers, failed registration, other users/configurations, and remote machines can be outside the
scope. An unreadable record makes coverage incomplete. Discovery does not broaden permissions or
scan process command lines/environments. Cooperative stop still requires fresh verified identity.

`workers` is read-only. For launching see [worker.md](worker.md); for item action discovery see
[actions.md](actions.md). OS service installation and startup management are operator-managed.
9 changes: 7 additions & 2 deletions skills/wrighty/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
---
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 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, 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.16.0 -->
<!-- wrighty-skill-version: 0.17.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
[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.

## Workflow

1. Verify `wrighty` is callable.
Expand Down
83 changes: 83 additions & 0 deletions skills/wrighty/references/board-and-workers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Board and worker overview

Use these workflows when the user asks to see their Wrighty board, triage blocked work, discover
workers, or assess whether a worker could pick up a named item. These are observations, not
permission to change items or launch processes.

## Board by workflow status

Run `wrighty list --json`. Its existing `result` array contains canonical IDs, workflow status,
priority, claims, retained sessions, and operational state. The additive `listing` block supplies:

- `statusOrder` and `statusOrderSource`: use configured column order when supplied. If the source
is `unknown`, group by the returned status names and say configured ordering is unavailable.
Do not invent a standard Todo/In Progress/Done workflow.
- `archiveScope`, `statusFilter`, `fields`, and `limit`: describe the scope actually requested.
- `returnedCount`, `countScope`, and `completeness`: counts describe returned items. A
`possibly-truncated` listing cannot establish whole-board totals.

For full-board counts, omit `--limit`, count the full returned array by workflow status, then
present a bounded selection with the total and omitted count. For a deliberately limited view,
use `--limit <n>` and label counts as that subset. Honor `--status`, `--archived`, and
`--include-archived` when requested. Never mix archived history into the default active board.
Show empty configured columns as zero only when the requested scope is complete and unfiltered.

Keep workflow columns separate from operational state. For example, an active-work column may
contain an item that is paused, awaiting clarification, or retry-scheduled. Include canonical IDs
in concise rows so follow-up requests identify the intended items unambiguously.

## Triage and available actions

Use `wrighty status --json` for operational groups, and `wrighty get <id> --json` to inspect a
specific blocker. Read `lastRun` and dispatch details before explaining what happened.
Retry-scheduled and handoff-queued work is deferred; do not classify it as awaiting clarification.

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.

## Workers and pickup prospects

Run `wrighty workers --json` for worker discovery alone. It reads the configuration-scoped local
registry and checks the recorded PIDs and process-start identities; it does not contact the
tracker or inspect retained worktrees. Each run has a `runId`. Several web-hosted runs may share
one PID, so do not collapse them into one worker.

The result includes observation time, scope/coverage, `localWorkers`, named `liveness`, `origin`,
`reportedState`, heartbeat, structured startup `instance.scheduling`, loop `instance.progress`, intake, remaining
allowance, and configuration drift. Report old or unreadable scheduling as unknown. Do not parse
`invocationSummary`, use current agent as the worker's only supported agent, or reconstruct its
startup settings from current repository configuration.

For a named item, run `wrighty workers --item <id> --json`. This additionally reads the tracker and
shared selection/admission evidence. For each run, read `pickup.outcome`, `code`, `message`,
`alreadyProcessing`, and `afterCurrentItem`:

- `alreadyProcessing`: identify that run/session; do not launch competing work.
- `could-pick-up`: observed selection allows pickup, possibly after its current item. This is
neither a reservation nor a timing guarantee; another item may be selected first.
- `cannot-pick-up`: explain the reason, such as closed intake, exhausted allowance, a different
targeted item, filters, claim ownership, workflow state, or provider deferral.
- `unknown`: explain the missing evidence. Do not equate it with absence or launch a replacement
automatically. Older registrations, sandbox denial, configuration drift, and inaccessible
workspace evidence can produce this result.

An empty complete scope means no registrations were observed in this local configuration.
Unregistered workers, other configurations/accounts, and remote workers may still exist.
Incomplete/unavailable coverage cannot establish even that scoped absence. A stale registration
is not evidence of open intake. Capability fields do not waive fresh identity checks for control.

Assessment describes the item's current state, not its state after a proposed Queue/Resume action.
After an authorized mutation, re-read the item and assessment before describing pickup prospects.
Queueing and launching are separate permissions. With no suitable observed worker, explain the
reason and offer an appropriate bounded or continuous launch only if the user's intent calls for
it. A next-item `worker --once` does not target a named item; `worker --item <id>` does.

Waiting must be bounded or explicitly hosted by the agent platform. Recheck only when asked to
wait or when confirming an authorized operation, and stop on meaningful progress, failure, or a
needed user decision. A conversation is not a guarantee of unattended monitoring. Worker-spawned
implementation sessions must not recursively launch workers. Never run a paid provider probe
merely to strengthen an assessment.
24 changes: 13 additions & 11 deletions src/Highbyte.Wrighty.Cli/CliApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ private RootCommand BuildRootCommand()
root.Subcommands.Add(BuildInitCommand());
root.Subcommands.Add(BuildListCommand());
root.Subcommands.Add(BuildStatusCommand());
root.Subcommands.Add(BuildWorkersCommand());
root.Subcommands.Add(BuildGetCommand());
root.Subcommands.Add(BuildActionsCommand());
root.Subcommands.Add(BuildContextCommand());
Expand Down Expand Up @@ -2753,24 +2754,25 @@ private Command BuildListCommand()
2);
}

var items = await tracker.ListOperationalAsync(
config,
new ListWorkItemsRequest(
var scope = ArchiveScope.Active;
if (parseResult.GetValue(archived))
scope = ArchiveScope.Archived;
else if (parseResult.GetValue(includeArchived))
scope = ArchiveScope.All;
var request = new ListWorkItemsRequest(
parseResult.GetValue(status),
parseResult.GetValue(limit),
parseResult.GetValue(archived)
? ArchiveScope.Archived
: parseResult.GetValue(includeArchived)
? ArchiveScope.All
: ArchiveScope.Active,
scope,
ParseFields(parseResult.GetValue(fields), allowDeletion: false)
.ToDictionary(pair => pair.Key, pair => pair.Value!, StringComparer.Ordinal)),
cancellationToken);
.ToDictionary(pair => pair.Key, pair => pair.Value!, StringComparer.Ordinal));
var items = await tracker.ListOperationalAsync(config, request, cancellationToken);
var listing = parseResult.GetValue(json)
? await DescribeListingAsync(config, request, items.Count, cancellationToken) : null;
await writer.WriteOperationalItemsAsync(
items,
parseResult.GetValue(compact),
parseResult.GetValue(json),
id => tracker.FormatShort(config, id));
id => tracker.FormatShort(config, id), listing);
},
cancellationToken));
return command;
Expand Down
26 changes: 26 additions & 0 deletions src/Highbyte.Wrighty.Cli/CliListingContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Highbyte.Wrighty.Cli.Output;
using Highbyte.Wrighty.Configuration;
using Highbyte.Wrighty.Errors;
using Highbyte.Wrighty.Models;

namespace Highbyte.Wrighty.Cli;

public sealed partial class CliApplication
{
private async Task<WorkItemListingContext> DescribeListingAsync(TrackerConfig config,
ListWorkItemsRequest request, int count, CancellationToken cancellationToken)
{
IReadOnlyList<string> statuses;
try
{
statuses = await tracker.Backend(config).WorkflowStatusesAsync(config, cancellationToken);
}
catch (Exception exception) when (exception is TrackerException or IOException or UnauthorizedAccessException)
{
statuses = [];
}
return new(statuses, statuses.Count > 0 ? "configured" : "unknown",
request.ArchiveScope.ToString().ToLowerInvariant(), request.Status, request.Fields,
request.Limit, count, request.Limit is { } limit && count >= limit ? "possibly-truncated" : "complete");
}
}
45 changes: 45 additions & 0 deletions src/Highbyte.Wrighty.Cli/CliWorkerDiscoveryCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.CommandLine;
using Highbyte.Wrighty.Configuration;
using Highbyte.Wrighty.Workers;

namespace Highbyte.Wrighty.Cli;

public sealed partial class CliApplication
{
private Command BuildWorkersCommand()
{
var json = JsonOption();
var item = new Option<string?>("--item") { Description = "Assess whether each observed worker could pick up this item." };
var command = new Command("workers", "Inspect registered local workers without starting or controlling them");
command.Options.Add(json);
command.Options.Add(item);
command.SetAction((parsed, cancellationToken) => ExecuteAsync(parsed.GetValue(json),
config => InspectWorkersAsync(config, parsed.GetValue(item), parsed.GetValue(json), cancellationToken),
cancellationToken));
return command;
}

private async Task InspectWorkersAsync(TrackerConfig config, string? item, bool json,
CancellationToken cancellationToken)
{
var configurationPath = config.SourcePath ?? Path.Combine(workingDirectory, TrackerConfigLoader.FileName);
var snapshot = await workerInstances.InspectAsync(configurationPath, cancellationToken);
var revision = await StatusConfigurationRevisionAsync(config, cancellationToken);
var state = item is null ? null : await tracker.GetOperationalAsync(config,
tracker.ResolveId(config, item), cancellationToken);
var workers = new List<WorkerDiscoveryEntry>();
foreach (var status in snapshot.Workers)
{
WorkerPickupAssessment? pickup = null;
if (state is not null)
pickup = workerService is null
? new(status.Instance.RunId, state.Item.Id.Value, "unknown", "ASSESSMENT_UNAVAILABLE",
"Worker selection assessment is unavailable here.")
: await workerService.AssessPickupAsync(config, status, state, revision,
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);
}
}
4 changes: 3 additions & 1 deletion src/Highbyte.Wrighty.Cli/Output/OutputWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,16 @@ public async Task WriteOperationalItemsAsync(
IEnumerable<WorkItemOperationalState> items,
bool compact,
bool json,
Func<WorkItemId, string> formatShort)
Func<WorkItemId, string> formatShort,
WorkItemListingContext? listing = null)
{
var materialized = items.ToArray();
if (json)
{
await WriteJsonAsync(new
{
schemaVersion = 1,
listing,
result = materialized
.Select(item => OperationalDto(item, formatShort))
.ToArray()
Expand Down
14 changes: 14 additions & 0 deletions src/Highbyte.Wrighty.Cli/Output/WorkItemListingContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Highbyte.Wrighty.Cli.Output;

public sealed record WorkItemListingContext(
IReadOnlyList<string> StatusOrder,
string StatusOrderSource,
string ArchiveScope,
string? StatusFilter,
IReadOnlyDictionary<string, string>? Fields,
int? Limit,
int ReturnedCount,
string Completeness)
{
public string CountScope { get; } = "returned-items";
}
Loading