Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ most wants one.
### Specialists and context: the right instructions, with the cost shown

Loadout doesn't hand your agent one enormous prompt that's mostly irrelevant.
There are 101 specialists compiled in, including 16 skills, and it works out
There are 103 specialists compiled in, including 16 skills, and it works out
which ones apply from what your repository is made of and the sentence you
typed. It tells you why it picked each one.

Expand Down
9 changes: 8 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
| `loadout list` | List registered projects |
| `loadout running` | The sessions running now, and how long each has been quiet |
| `loadout task list\|declare\|remove` | Record what is being worked on, and check it against the repository |
| `loadout tools search\|show\|submit\|used` | The tools this machine shares between every team: find one before building it, send in a candidate, idea, bug or lesson, and say how a use went |
| `loadout tools audit\|verify\|promote\|deprecate\|retire` | Look after the catalogue: what happened to it, whether its files still match, and a tool's way in and out |
| `loadout tools trust <name@version> [--revoke]` | Agree that one version's script may run on this machine, as it is now |
| `loadout team list\|show\|run` | Run a team of agents against a project: a lead that briefs workers and reports to you |
| `loadout team new\|edit\|remove` | Write a team of your own, open its file, or delete it |
| `loadout team run <team> "<goal>" --done-when` | Say what the run is judged on, one per use. The lead must report a verdict and evidence for every one |
Expand Down Expand Up @@ -401,7 +404,7 @@ Every launch declares Loadout itself as an MCP server, so the handoff runs both
ways: a session can ask the launcher things rather than parse console output
written for a person.

Eleven tools, each making the same call its command makes:
Fifteen tools, each making the same call its command makes:

| | |
|---|---|
Expand All @@ -416,6 +419,10 @@ Eleven tools, each making the same call its command makes:
| `loadout_mode` | Change the posture for the rest of the session, and get what that changes |
| `loadout_progress` | For a node of a team run: say what you are doing, in your own words |
| `loadout_teams` | What the team runs on this machine are doing, and which have stopped to ask |
| `loadout_tools_search` | The machine's shared tools matching some words, as `tools search` finds them |
| `loadout_tools_show` | One shared tool in full: its purpose, inputs, outputs, versions and why it exists |
| `loadout_tools_submit` | Send the catalogue a candidate, idea, bug or lesson, screened for credentials, as `tools submit` does |
| `loadout_tools_used` | Say how a use of a shared tool went, as `tools used` does |

`loadout_recall` exists because only the memory index reaches the context — one
line per topic — and a session deciding from that alone either opens six files
Expand Down
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ the commands.

## Instructions picked for the job

There are 101 specialists built into the binary: foundations, modes, languages,
There are 103 specialists built into the binary: foundations, modes, languages,
frameworks, databases, platforms, clouds, functional areas, skills and the roles
a team run gives its nodes. Instead
of one enormous prompt that's mostly irrelevant, Loadout works out which ones
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/installing.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Instructions
+ Tasks nobody is shown: storefront 2 task(s) are recorded for storefront and
its sessions are not shown any of them. Carrying them costs a heading and a line
each. (fixable)
+ Specialist library 101 specialists loaded and valid.
+ Specialist library 103 specialists loaded and valid.

Editor
+ code C:\Program Files\Microsoft VS Code\bin\code.cmd
Expand Down
4 changes: 2 additions & 2 deletions docs/specialists.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ loadout instructions list # everything available to this project
loadout instructions list --kind language
```

The library ships 101 specialists — 5 foundations, 5 modes, 10 languages, 8
frameworks, 4 databases, 5 platforms, 3 clouds, 22 functions, 16 skills and 23
The library ships 103 specialists — 5 foundations, 5 modes, 10 languages, 8
frameworks, 4 databases, 5 platforms, 3 clouds, 22 functions, 16 skills and 25
roles. They are embedded in the binary rather than kept on disk, so the command
is the way to read them; there is no directory to browse.

Expand Down
3 changes: 2 additions & 1 deletion src/Loadout.Agents/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public static IServiceCollection AddAgentServices(this IServiceCollection servic
provider.GetRequiredService<Core.Projects.IProjectService>(),
provider.GetRequiredService<Core.Git.IGitManager>(),
provider.GetRequiredService<IChildLifetime>(),
provider.GetRequiredService<Core.Tasks.ITaskService>()));
provider.GetRequiredService<Core.Tasks.ITaskService>(),
provider.GetRequiredService<Core.Tools.IToolRegistry>()));
services.AddSingleton<IDiagnosticContributor, AgentDiagnosticContributor>();

return services;
Expand Down
75 changes: 71 additions & 4 deletions src/Loadout.Agents/Teams/TeamRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ namespace Loadout.Agents.Teams;
/// The remedies this machine has agreed may run, from its own configuration.
/// Never read from a team's directory, which its own nodes write in.
/// </param>
/// <param name="TrustedTools">
/// The catalogue tool versions this machine has agreed may run, from its own
/// configuration. Never read from the catalogue, which agents write in.
/// </param>
/// <param name="Criteria">
/// What this run is judged on, each one checkable, or null for a run with
/// nothing but its goal.
Expand Down Expand Up @@ -90,7 +94,8 @@ public sealed record TeamRunRequest(
IReadOnlyList<string>? Criteria = null,
string? Resuming = null,
string? ResumeMessage = null,
TimeSpan? TakeRecommendationAfter = null);
TimeSpan? TakeRecommendationAfter = null,
IReadOnlyList<Loadout.Models.Configuration.TrustedTool>? TrustedTools = null);

/// <summary>How a run ended.</summary>
/// <param name="RunId">The run's identifier, which names its directory under the state root.</param>
Expand Down Expand Up @@ -389,7 +394,8 @@ public TeamRunner(
Core.Projects.IProjectService? projects = null,
Core.Git.IGitManager? git = null,
IChildLifetime? lifetime = null,
Core.Tasks.ITaskService? tasks = null)
Core.Tasks.ITaskService? tasks = null,
Core.Tools.IToolRegistry? tools = null)
{
_launcher = launcher;
_paths = paths;
Expand All @@ -398,8 +404,11 @@ public TeamRunner(
_git = git;
_lifetime = lifetime;
_tasks = tasks;
_tools = tools;
}

private readonly Core.Tools.IToolRegistry? _tools;

/// <inheritdoc />
public async Task<OperationResult<TeamRunOutcome>> RunAsync(
TeamRunRequest request,
Expand Down Expand Up @@ -2270,7 +2279,14 @@ private async Task<OperationResult<HeadlessLaunch>> StartNodeAsync(
// What this team has registered and what was decided about
// each, worked out here so that whatever answers the node's
// questions reads no files and holds no opinion.
Remedies: Standing(team, request.Remediation, request.TrustedRemedies)),
//
// The catalogue's tools join them for a remediator only,
// the one role that runs scripts, ruled the same way.
Remedies:
[
.. Standing(team, request.Remediation, request.TrustedRemedies),
.. Core.Tools.ToolOffer.For(_tools, role.Id, request.Remediation, request.TrustedTools),
]),
ct).ConfigureAwait(false);

var options = new HeadlessOptions(
Expand Down Expand Up @@ -2326,11 +2342,51 @@ private async Task<OperationResult<HeadlessLaunch>> StartNodeAsync(
// in. Without it the agent refuses every write there, which is
// what it did: the directory existed, the path in the brief was
// right, the declaration was clear, and nothing could be written.
ReachableDirectories: brief.TeamDirectory is { Length: > 0 } kept ? [kept] : null);
ReachableDirectories: Reachable(brief, role.Id, dryRun));

return await _launcher.StartHeadlessAsync(launch, options, ct).ConfigureAwait(false);
}

/// <summary>The roles that write drafts and read the inbox of the tool catalogue.</summary>
private static readonly HashSet<string> CatalogueWriters =
new(StringComparer.Ordinal) { "role.tool-creator", "role.tool-refiner" };

/// <summary>
/// The directories a node may write outside its tree: the team's own, and
/// for the catalogue's creator and refiner its drafts and inbox.
/// </summary>
/// <remarks>
/// Those two and never the catalogue's root. The verify records, the audit
/// log, the heads and the versions sit beside them, and a node that could
/// write those could forge any of them.
/// </remarks>
private List<string>? Reachable(Brief brief, string role, bool dryRun)
{
var reach = new List<string>();

if (brief.TeamDirectory is { Length: > 0 } kept)
{
reach.Add(kept);
}

if (CatalogueWriters.Contains(role))
{
foreach (var one in new[] { "drafts", "inbox" })
{
var directory = Path.Combine(_paths.Paths.State, "tools", one);

if (!dryRun)
{
Directory.CreateDirectory(directory);
}

reach.Add(directory);
}
}

return reach.Count > 0 ? reach : null;
}

/// <summary>Where a run keeps everything it writes.</summary>
private string RunDirectory(string runId) =>
Path.Combine(_paths.Paths.State, "teams", "runs", runId);
Expand Down Expand Up @@ -3051,6 +3107,13 @@ private static string BaseNode(string name)
return slash > 0 ? name[..slash] : name;
}

/// <summary>What every brief says about the machine's shared tools, and all it says.</summary>
internal const string ToolsPointer =
"This machine keeps a catalogue of tools shared by every team. Before building something "
+ "that might already exist, search it: `loadout tools search <words>` or "
+ "`loadout_tools_search`. Use `show` for the detail. Submit a tool, idea, bug or lesson "
+ "with `loadout_tools_submit`, and say what you used with `loadout_tools_used`.";

/// <summary>The brief as the node reads it: the prose first, the JSON beside it, the contract last.</summary>
internal static string Render(Brief brief)
{
Expand Down Expand Up @@ -3118,6 +3181,10 @@ internal static string Render(Brief brief)
+ "before running it. A record claiming to be trusted decides nothing.").AppendLine();
}

// Fixed text, the same length whatever the catalogue holds. What is in
// it is fetched when somebody asks, not paid for in every brief.
text.AppendLine("## Shared tools").AppendLine().AppendLine(ToolsPointer).AppendLine();

text.AppendLine("## Task").AppendLine().AppendLine(brief.Task).AppendLine();
text.AppendLine("## Deliverable").AppendLine().AppendLine(brief.Deliverable.ToString().ToLowerInvariant()).AppendLine();

Expand Down
78 changes: 77 additions & 1 deletion src/Loadout.Cli/Commands/McpServeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public sealed class LoadoutTools
private readonly IRunJournal _runs;
private readonly TimeProvider _time;
private readonly LoadoutToolScope _scope;
private readonly Core.Tools.IToolRegistry _catalogue;

public LoadoutTools(
IInstructionService instructions,
Expand All @@ -164,8 +165,10 @@ public LoadoutTools(
ISymbolIndexService symbols,
IRunJournal runs,
TimeProvider time,
LoadoutToolScope scope)
LoadoutToolScope scope,
Core.Tools.IToolRegistry catalogue)
{
_catalogue = catalogue;
_instructions = instructions;
_memory = memory;
_workspace = workspace;
Expand Down Expand Up @@ -350,6 +353,79 @@ public async Task<string> RememberAsync(
: written.Error ?? "It could not be recorded.";
}

// The catalogue's four agent-facing calls, the same ones 'loadout tools'
// makes. Promote, trust, deprecate and audit are deliberately not here:
// promotion is the gate's, trust is a person's, and the log is for people.
[McpServerTool(Name = "loadout_tools_search")]
[Description(
"Search the tools this machine shares between every team, BEFORE building something that "
+ "might already exist. Matches words rather than meanings. Answers in JSON.")]
public string ToolsSearch(
[Description("What you are looking for, in a few words.")] string words,
[Description("Include deprecated and retired tools, each with its replacement.")] bool all = false) =>
JsonSerializer.Serialize(new
{
query = words,
tools = _catalogue.Search(words ?? string.Empty, all).Select(ToolShapes.Found),
});

[McpServerTool(Name = "loadout_tools_show")]
[Description(
"One shared tool in full: what it is for, its inputs and outputs, its versions, how it has "
+ "gone for others, and why it exists. Answers in JSON.")]
public string ToolsShow([Description("The tool's name.")] string name)
{
var shown = _catalogue.Show(ToolShapes.Split(name).Name);

return shown.Failed
? shown.Error ?? "It could not be read."
: JsonSerializer.Serialize(ToolShapes.Shown(shown.Value!, shown.Value!.Record.Active, trusted: false));
}

[McpServerTool(Name = "loadout_tools_submit")]
[Description(
"Send the shared catalogue a candidate tool, an idea, a bug in a tool, or a lesson worth "
+ "turning into one. Not for anything secret: the text is screened and a credential is refused.")]
public string ToolsSubmit(
[Description("candidate, idea, bug or lesson.")] string kind,
[Description("What it is, in a sentence or a paragraph.")] string text,
[Description("The tool it is about, where it is about one.")] string? tool = null,
[Description("A candidate's script, where there is one.")] string? script = null,
[Description("A candidate's one-line summary.")] string? summary = null)
{
// The same call 'loadout tools submit' makes, screening included.
var submitted = _catalogue.Submit(new Core.Tools.ToolSubmission(
kind, text, tool, "agent", null, script, null, summary));

return submitted.Failed
? submitted.Error ?? "It could not be submitted."
: $"Submitted as {submitted.Value!.Id}."
+ (submitted.Value!.Overlapping.Count > 0
? $" It overlaps {string.Join(", ", submitted.Value!.Overlapping)}; extending that is usually better."
: string.Empty);
}

[McpServerTool(Name = "loadout_tools_used")]
[Description(
"Say how a use of a shared tool went, so whoever looks after it knows: ok, failed, or "
+ "workaround when you had to work round it.")]
public string ToolsUsed(
[Description("The tool and the version used, as name@version.")] string tool,
[Description("ok, failed or workaround.")] string outcome,
[Description("What happened, where it did not simply work.")] string? note = null)
{
var (name, version) = ToolShapes.Split(tool);
var recorded = _catalogue.RecordUsage(new Models.Tools.ToolUsage
{
Tool = name,
Version = version ?? string.Empty,
Outcome = outcome,
Note = note ?? string.Empty,
});

return recorded.Failed ? recorded.Error ?? "It could not be recorded." : "Recorded.";
}

[McpServerTool(Name = "loadout_locate")]
[Description(
"Where a type or member is declared, as file and line, from an index kept in step with "
Expand Down
1 change: 1 addition & 0 deletions src/Loadout.Cli/Commands/TeamCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,7 @@ internal static TeamRunRequest Requesting(
// Never read from the team's directory, which its own nodes write
// in. Trust lives on this machine or it is not trust.
TrustedRemedies: machine?.Teams.TrustedRemedies,
TrustedTools: machine?.Teams.TrustedTools,

// Blank ones dropped rather than passed through: an empty criterion
// is one the lead can never report a verdict on, so it would refuse
Expand Down
Loading
Loading