Add schedules and context locality telemetry

This commit is contained in:
thePR0M3TH3AN committed 2026-09-27 17:16:37 -04:00
1 parent d2837581d9
commit 55a4981567
17 files changed
+729 -15

No files matched your search

+27 -1
View File
@@ -54,9 +54,13 @@ and the first Claude reference-fleet path:
and backlog inventory while refusing to invent missing ownership boundaries.
- a redesigned public product site and read-only local Fleet Console backed by
the same repository and runtime state rather than a separate dashboard model.
- identity-bound context-usage telemetry that separates measurements from
estimates and relates cost/cache behavior to verified work;
- validated system/session schedules with shell-free actions, authority,
retries, durable run evidence, and explicit approval for mutation.
Live mixed-provider qualification, stable self-host bootstrapping, COMBATRIG
cutover, scheduling, richer measured context telemetry, and public deployment
cutover, persistent system-schedule launcher integration, and public deployment
remain staged work.
The canonical architecture and delivery plan is
@@ -191,6 +195,28 @@ or mutation. Automation can obtain the same read-only view with:
node bin/torch.mjs console snapshot --repo /path/to/project --json
```
Runtime adapters and agents can report cache locality without conflating
provider measurements and estimates:
```bash
torch context record --area backend --source provider-response \
--measurement measured --cache-read 24000 --uncached-input 6000 \
--cost-microusd 420000 --verified-items 1 --json
torch context report --area backend --json
```
Configured schedules remain machine-readable and authority-gated:
```bash
torch schedules list --actor session-manager --json
torch schedules plan --id fleet-hygiene --actor session-manager --json
torch schedules run --id fleet-hygiene --actor session-manager --json
```
Mutating schedules additionally require `--yes`. Cron definitions are exposed
for an external system launcher; TORCH does not silently install persistent
timers.
## Product principles
- Git is required; GitHub is optional.
+17
View File
@@ -1061,6 +1061,13 @@ initializing a missing database. Context-locality output explicitly reports
`unavailable` until runtime usage samples exist, preventing estimates from
masquerading as measurements.
Identity-bound context telemetry now accepts provider measurements or explicit
estimates, never an unlabeled mixture. It records cached and uncached input,
cache creation and reads, compactions, resumed prompt size, micro-USD cost,
task/commit references, and verified item counts. Reports keep evidence classes
separate, derive cache-read ratio and cost per verified item, and warn when
usage has no verified outcome.
## 22. Scheduling
TORCH recognizes two schedule classes.
@@ -1086,6 +1093,16 @@ Every schedule MUST declare:
Schedules MUST be generated or validated from machine-readable configuration
so launcher code and prose cannot silently disagree.
Implementation checkpoint (2026-09-27): installed configuration carries
validated system and session schedules with owner, trigger, behavior,
shell-free action, authority, retry, failure recipient, and source-of-truth
fields. The service plans without mutation, blocks session schedules while the
Session Manager is offline, requires explicit approval for mutating actions,
records durable run evidence, retries deterministically, and reports failures
through durable blocker messages. CLI and MCP surfaces share the service.
Cron definitions currently require an external launcher; persistent timer
installation remains open and will use the normal ownership manifest.
## 23. Repository and forge operating modes
### 23.1 Local-only mode
+7 -3
View File
@@ -23,8 +23,10 @@
"./mcp": "./src/mcp/tools.mjs",
"./observability": "./src/observability/snapshot.mjs",
"./resources": "./src/resources/service.mjs",
"./schedules": "./src/schedules/service.mjs",
"./runtime/lifecycle": "./src/runtime/lifecycle.mjs",
"./self-host": "./src/self-host/service.mjs"
"./self-host": "./src/self-host/service.mjs",
"./telemetry/context": "./src/telemetry/context.mjs"
},
"bin": {
"torch": "./bin/torch.mjs",
@@ -62,7 +64,9 @@
"src/observability/",
"src/resources/",
"src/runtime/",
"src/schedules/",
"src/self-host/",
"src/telemetry/",
"docs/PORTABLE_AGENT_FLEET_SPEC.md",
"README.md",
"LICENSE",
@@ -74,8 +78,8 @@
"torch": "node bin/torch.mjs",
"check": "node --check bin/torch.mjs && node --check src/cli.mjs && node --check src/mcp/server.mjs && node --check src/console/server.mjs && node --check site/app.js && node --check site/console.js",
"site:serve": "node bin/torch.mjs console serve",
"format": "eslint src/adapters src/backlog src/canonical src/checks src/console src/control-plane src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/self-host src/cli.mjs bin/torch.mjs site/*.js test/fleet --fix",
"lint": "eslint src/adapters src/backlog src/canonical src/checks src/console src/control-plane src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/self-host src/cli.mjs bin/torch.mjs site/*.js test/fleet"
"format": "eslint src/adapters src/backlog src/canonical src/checks src/console src/control-plane src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet --fix",
"lint": "eslint src/adapters src/backlog src/canonical src/checks src/console src/control-plane src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet"
},
"dependencies": {
"@modelcontextprotocol/server": "^2.1.0",
@@ -150,6 +150,18 @@ test_integrity_note:
given: an installed Fleet without an initialized control-plane database
when: the observation API and local HTTP console are requested
then: fixed assets and structured state are served with restrictive headers while Git and machine-local state remain unchanged
- id: SCN-context-locality
given: measured provider usage and a separate heuristic estimate for one persistent Fleet identity
when: CLI and identity-bound MCP record and aggregate the samples
then: evidence classes stay separate, forged identities fail, and cache ratio plus cost per verified item are derived without calling zero-output usage efficient
- id: SCN-schedule-boundaries
given: system and session schedules with different owners, behavior, retries, and authority
when: schedules are planned and executed through a virtualized shell-free runner
then: offline sessions and unauthorized actors are blocked, mutation needs approval, failures retry exactly and notify durably, and each run is recorded
- id: SCN-cli-schedules
given: an installed Fleet with an active Session Manager and a read-only session schedule
when: public CLI commands list, plan, and run it
then: the same validated definition executes without shell interpretation and emits durable run evidence
observable_outcomes:
- Git status before and after read-only analysis
- presence or absence of .torch and XDG-local project state
@@ -160,6 +172,8 @@ test_integrity_note:
- compatibility report counts and unchanged COMBATRIG Git status
- HTTP status, content-security policy, fixed routes, and traversal refusal
- unchanged Git status and absent database before and after console observation
- measured and estimated context groups, cache-read ratio, and cost per verified item
- schedule plan blockers, subprocess shell flag, retry count, and durable failure message
- preservation of project source and modified owner configuration
- SQLite state after process restart
- CLI and MCP output over real process boundaries
@@ -187,6 +201,7 @@ test_integrity_note:
- external temporary Markdown specification with no provider or network calls
- synthetic COMBATRIG layout in an isolated temporary Git repository
- loopback-only ephemeral HTTP server and fixed static route map
- injected schedule executor, fixed clock, bounded retries, and no sleeps
anti_cheat_rationale:
prevents:
- init that silently writes files
@@ -218,6 +233,9 @@ test_integrity_note:
- COMBATRIG compatibility inventing path ownership to force a green migration
- a disconnected dashboard inventing state or creating it during observation
- public copy claiming live qualification that only virtualized tests prove
- estimated token counts being presented as provider measurements
- low token use being called efficient without a verified outcome
- schedule prose diverging from executable definitions or mutating without approval
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
+54
View File
@@ -23,6 +23,8 @@ import { planFleetDown, planFleetUp, startFleet, stopFleet } from './runtime/lif
import { ResourceService } from './resources/service.mjs';
import { CANDIDATE_ACCEPTANCE_SCENARIOS, createVersionService } from './self-host/service.mjs';
import { observeProject } from './observability/snapshot.mjs';
import { ContextTelemetryService } from './telemetry/context.mjs';
import { ScheduleService } from './schedules/service.mjs';
const HELP = `TORCH — portable agent fleet
@@ -80,6 +82,12 @@ Usage:
torch import combatrig --source <path> [--output <path>] [--json]
torch console snapshot [--repo <path>] [--json]
torch console serve [--repo <path>] [--host <address>] [--port <n>]
torch context report [--area <id>] [--task <id>] [--commit <sha>] [--measurement <class>] [--json]
torch context record --area <id> --source <text> --measurement <measured|estimated> [usage fields] [--json]
torch schedules list [--actor <id|owner>] [--json]
torch schedules runs [--id <schedule>] [--json]
torch schedules plan --id <schedule> --actor <id|owner> [--json]
torch schedules run --id <schedule> --actor <id|owner> [--yes] [--json]
torch doctor [--json]
torch uninstall [--dry-run] [--purge] [--json]
`;
@@ -169,6 +177,7 @@ function runCandidateAcceptance(candidateRoot, spawn) {
['detach-uninstall', ['SCN-install-doctor-purge']],
['combatrig-compatibility', ['SCN-combatrig-import', 'SCN-cli-combatrig-import']],
['product-surface', ['SCN-product-site', 'SCN-console-readonly']],
['operations-observability', ['SCN-context-locality', 'SCN-schedule-boundaries', 'SCN-cli-schedules']],
]);
const scenarios = CANDIDATE_ACCEPTANCE_SCENARIOS.filter((scenario) =>
(evidenceByScenario.get(scenario) ?? []).every((marker) => testOutput.includes(marker)));
@@ -558,6 +567,51 @@ export async function runCli(argv = process.argv.slice(2), {
control.close();
}
}
if (command === 'context') {
const operation = argv[1] ?? 'report';
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const telemetry = new ContextTelemetryService({ controlPlane: control });
if (operation === 'report') print(telemetry.report({
areaId: optionValue(argv, '--area'), task: optionValue(argv, '--task'),
commit: optionValue(argv, '--commit'), measurement: optionValue(argv, '--measurement'),
}), { json });
else if (operation === 'record') print(telemetry.record({
areaId: optionValue(argv, '--area'), source: optionValue(argv, '--source'),
measurement: optionValue(argv, '--measurement'), runtime: optionValue(argv, '--runtime'),
runtimeSessionId: optionValue(argv, '--session'), task: optionValue(argv, '--task'),
commit: optionValue(argv, '--commit'), cachedInput: optionValue(argv, '--cached-input'),
uncachedInput: optionValue(argv, '--uncached-input'), cacheCreation: optionValue(argv, '--cache-creation'),
cacheRead: optionValue(argv, '--cache-read'), compactions: optionValue(argv, '--compactions'),
resumedPromptBytes: optionValue(argv, '--resumed-prompt-bytes'), costMicrousd: optionValue(argv, '--cost-microusd'),
verifiedItems: optionValue(argv, '--verified-items'),
}), { json });
else throw new TorchError(`Unknown context operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
return 0;
} finally {
control.close();
}
}
if (command === 'schedules') {
const operation = argv[1] ?? 'list';
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const schedules = new ScheduleService({ repositoryRoot: repository.root, controlPlane: control });
const scheduleId = optionValue(argv, '--id');
const actorId = optionValue(argv, '--actor') ?? 'owner';
if (operation === 'list') print({ schedules: schedules.list({ actorId }) }, { json });
else if (operation === 'runs') print({ runs: schedules.runs({ scheduleId }) }, { json });
else if (operation === 'plan') print(schedules.plan({ scheduleId, actorId }), { json });
else if (operation === 'run') {
const result = schedules.run({ scheduleId, actorId, approved: argv.includes('--yes') });
print(result, { json });
return result.result === 'succeeded' ? 0 : 1;
} else throw new TorchError(`Unknown schedules operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
return 0;
} finally {
control.close();
}
}
if (command === 'checks') {
const operation = argv[1] ?? 'list';
const control = openControlPlane({ repositoryRoot: repository.root, env });
+18 -4
View File
@@ -139,6 +139,22 @@ export function analyzeCombatrigFleet({ repository } = {}) {
const checks = packageChecks(root);
const operationsPath = join(root, 'docs', 'agents', 'OPERATIONS.md');
const operations = existsSync(operationsPath) ? readFileSync(operationsPath, 'utf8') : '';
const schedules = [
{
id: 'release', title: 'Owner-approved release', owner: 'owner', lifetime: 'system',
trigger: { type: 'cron', expression: '0 8,16 * * *' }, behavior: 'mutating',
action: { type: 'command', command: './tools/dev/release.sh', args: [] },
required_authority: ['owner'], retry: { max_attempts: 1 }, failure_recipient: 'session-manager',
source_of_truth: 'docs/agents/OPERATIONS.md', detected: operations.includes('0 8,16 * * *'),
},
{
id: 'dispatcher-hygiene', title: 'Session fleet hygiene', owner: 'session-manager', lifetime: 'session',
trigger: { type: 'interval', seconds: 900 }, behavior: 'read-only',
action: { type: 'command', command: 'torch', args: ['doctor', '--json'] },
required_authority: ['session-manager'], retry: { max_attempts: 1 }, failure_recipient: 'session-manager',
source_of_truth: 'docs/agents/OPERATIONS.md', detected: operations.includes('Session-local schedules'),
},
];
return {
schema: 'torch.dev/domain-proposal/v1alpha1',
generatedAt: new Date().toISOString(),
@@ -162,16 +178,14 @@ export function analyzeCombatrigFleet({ repository } = {}) {
id: 'browser-renderer', capacity: gateCapacity(root), queue: 'fifo', max_hold_seconds: 10800,
evidence: 'tools/test/serve.mjs',
}],
schedules,
architecture: { components: [], dependencies: [], sharedSurfaces: [], verificationSurfaces: [], operationalSurfaces: [] },
compatibility: {
source: 'combatrig-portable-fleet', sourceRoster: 'docs/agents/roster.json',
areas: { total: converted.length, domains: domains.length, managerLegacyId: manager?.legacy_id ?? null },
backlog: backlogInventory(root),
checkCatalog: checks.catalog,
schedules: [
{ id: 'release', lifetime: 'system', authority: 'owner', expression: '0 8,16 * * *', command: 'tools/dev/release.sh', detected: operations.includes('0 8,16 * * *') },
{ id: 'dispatcher-hygiene', lifetime: 'session', authority: 'session-manager', detected: operations.includes('Session-local schedules') },
],
schedules,
release: { provider: 'custom-command', authority: 'owner', command: 'tools/dev/release.sh' },
worktrees: converted.map((area) => ({ areaId: area.id, ...area.worktree_status })),
migrationGaps: gaps,
+9
View File
@@ -314,6 +314,7 @@ export function proposeDomains({ repository, analysis }) {
resources: analysis.inventory.scarceResources.map((id) => ({
id, capacity: 1, queue: 'fifo', max_hold_seconds: 3600,
})),
schedules: [],
architecture: graph,
};
}
@@ -360,6 +361,14 @@ export function validateApprovedProposal({ proposal, repository }) {
for (const collision of proposal?.collisions ?? []) {
if (!collision.resolution?.strategy) problems.push(`collision ${collision.domains?.join('/')} has no resolution`);
}
for (const schedule of proposal?.schedules ?? []) {
if (!schedule.id || !['system', 'session'].includes(schedule.lifetime)
|| !['read-only', 'mutating'].includes(schedule.behavior)
|| !schedule.owner || !schedule.trigger || !schedule.action
|| !schedule.required_authority?.length || !schedule.source_of_truth) {
problems.push(`schedule ${schedule.id ?? '<missing>'} is incomplete`);
}
}
if (proposal?.session_manager?.worktree_name !== undefined
&& proposal.session_manager.worktree_name !== null
&& !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(proposal.session_manager.worktree_name)) {
+1
View File
@@ -47,6 +47,7 @@ function initialFiles({ repository, projectId, createdAt, proposal }) {
},
checks: proposal.checks ?? [],
resources: proposal.resources ?? [],
schedules: proposal.schedules ?? [],
integration: {
provider: 'torch', target: repository.branch || 'main', require_current_main: true,
required_checks: (proposal.checks ?? []).map((check) => check.id),
+5 -1
View File
@@ -7,6 +7,8 @@ import { IntegrationService } from '../integration/service.mjs';
import { openControlPlane } from '../control-plane/service.mjs';
import { ResourceService } from '../resources/service.mjs';
import { createTorchMcpServer } from './tools.mjs';
import { ContextTelemetryService } from '../telemetry/context.mjs';
import { ScheduleService } from '../schedules/service.mjs';
function optionValue(argv, name) {
const direct = argv.find((argument) => argument.startsWith(`${name}=`));
@@ -25,8 +27,10 @@ const integrationService = new IntegrationService({ repositoryRoot, controlPlane
const backlogService = new BacklogService({
repositoryRoot, controlPlane, integrationLookup: (requestId) => integrationService.get(requestId),
});
const contextTelemetryService = new ContextTelemetryService({ controlPlane });
const scheduleService = new ScheduleService({ repositoryRoot, controlPlane });
const handle = serveStdio(() => createTorchMcpServer(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService,
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
}), {
onerror: (error) => console.error(`TORCH MCP: ${error.message}`),
});
+64 -3
View File
@@ -47,6 +47,12 @@ export const TORCH_MCP_TOOL_NAMES = Object.freeze([
'torch_authorize_integration',
'torch_plan_integration_landing',
'torch_land_integration',
'torch_record_context_usage',
'torch_context_report',
'torch_list_schedules',
'torch_schedule_runs',
'torch_plan_schedule',
'torch_run_schedule',
]);
function claimedIdentity(controlPlane, actorId, claim, field) {
@@ -65,7 +71,7 @@ function claimedIdentity(controlPlane, actorId, claim, field) {
}
export function createTorchToolset(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService,
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
} = {}) {
const tools = new Map([
['torch_identity', {
@@ -330,6 +336,61 @@ export function createTorchToolset(controlPlane, {
}),
});
}
if (contextTelemetryService) {
tools.set('torch_record_context_usage', {
description: 'Record measured or explicitly estimated context usage for the bound Fleet identity and its verified outcomes.',
schema: {
area_id: z.string().min(1).optional(), source: z.string().min(1), measurement: z.enum(['measured', 'estimated']),
runtime: z.string().min(1).optional(), runtime_session_id: z.string().min(1).optional(),
task: z.string().min(1).optional(), commit: z.string().min(1).optional(),
cached_input: z.number().int().nonnegative().optional(), uncached_input: z.number().int().nonnegative().optional(),
cache_creation: z.number().int().nonnegative().optional(), cache_read: z.number().int().nonnegative().optional(),
compactions: z.number().int().nonnegative().optional(), resumed_prompt_bytes: z.number().int().nonnegative().optional(),
cost_microusd: z.number().int().nonnegative().optional(), verified_items: z.number().int().nonnegative().optional(),
},
invoke: ({
area_id: areaId, runtime_session_id: runtimeSessionId, cached_input: cachedInput,
uncached_input: uncachedInput, cache_creation: cacheCreation, cache_read: cacheRead,
resumed_prompt_bytes: resumedPromptBytes, cost_microusd: costMicrousd,
verified_items: verifiedItems, ...input
}) => contextTelemetryService.record({
...input, runtimeSessionId, cachedInput, uncachedInput, cacheCreation, cacheRead,
resumedPromptBytes, costMicrousd, verifiedItems,
areaId: claimedIdentity(controlPlane, actorId, areaId, 'area_id'),
}),
});
tools.set('torch_context_report', {
description: 'Aggregate context locality by Fleet identity while keeping measured and estimated evidence separate.',
schema: {
area_id: z.string().min(1).optional(), task: z.string().min(1).optional(),
commit: z.string().min(1).optional(), measurement: z.enum(['measured', 'estimated']).optional(),
},
invoke: ({ area_id: areaId, ...input }) => contextTelemetryService.report({
...input, areaId: areaId ? claimedIdentity(controlPlane, actorId, areaId, 'area_id') : actorId,
}),
});
}
if (scheduleService) {
tools.set('torch_list_schedules', {
description: 'List validated schedules and their current authority, lifetime, and due status.',
schema: {}, invoke: () => ({ schedules: scheduleService.list({ actorId }) }),
});
tools.set('torch_schedule_runs', {
description: 'List durable run evidence for configured schedules.',
schema: { schedule_id: z.string().min(1).optional(), limit: z.number().int().min(1).max(1000).optional() },
invoke: ({ schedule_id: scheduleId, limit }) => ({ runs: scheduleService.runs({ scheduleId, limit }) }),
});
tools.set('torch_plan_schedule', {
description: 'Preview a configured schedule and its blockers without executing it.',
schema: { schedule_id: z.string().min(1) },
invoke: ({ schedule_id: scheduleId }) => scheduleService.plan({ scheduleId, actorId }),
});
tools.set('torch_run_schedule', {
description: 'Run a read-only configured schedule as the bound Fleet identity. Mutating schedules require owner CLI approval.',
schema: { schedule_id: z.string().min(1) },
invoke: ({ schedule_id: scheduleId }) => scheduleService.run({ scheduleId, actorId, approved: false }),
});
}
return tools;
}
@@ -341,7 +402,7 @@ export function callTorchTool(controlPlane, name, input = {}, options = {}) {
}
export function createTorchMcpServer(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService,
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
} = {}) {
controlPlane.assertIdentity(actorId);
const server = new McpServer(
@@ -351,7 +412,7 @@ export function createTorchMcpServer(controlPlane, {
},
);
for (const [name, tool] of createTorchToolset(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService,
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
})) {
server.registerTool(name, { description: tool.description, inputSchema: tool.schema }, async (input) => {
try {
+8 -3
View File
@@ -141,13 +141,18 @@ export function observeProject({ repositoryRoot, env = process.env, now = () =>
`);
if (hasTable(database, 'context_usage')) {
const samples = database.prepare(`
SELECT area_id AS areaId, SUM(cached_input) AS cachedInput, SUM(uncached_input) AS uncachedInput,
SELECT area_id AS areaId, measurement, SUM(cached_input) AS cachedInput, SUM(uncached_input) AS uncachedInput,
SUM(cache_creation) AS cacheCreation, SUM(cache_read) AS cacheRead,
SUM(compactions) AS compactions, SUM(resumed_prompt_bytes) AS resumedPromptBytes,
SUM(cost_microusd) AS costMicrousd, SUM(verified_items) AS verifiedItems
FROM context_usage GROUP BY area_id ORDER BY area_id
FROM context_usage GROUP BY area_id, measurement ORDER BY area_id, measurement
`).all();
result.contextLocality = { measured: true, status: 'measured', samples };
const measured = samples.some((sample) => sample.measurement === 'measured');
result.contextLocality = {
measured, status: measured ? 'measured' : 'estimated-only', samples,
warning: samples.some((sample) => sample.verifiedItems === 0)
? 'Usage without verified outcomes cannot establish efficiency.' : null,
};
}
} finally {
database.close();
+178
View File
@@ -0,0 +1,178 @@
import { randomUUID } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
import { TorchError } from '../kernel/errors.mjs';
function text(value, field) {
if (typeof value !== 'string' || !value.trim()) {
throw new TorchError(`Schedule ${field} is required`, { code: 'SCHEDULE_CONFIG_INVALID', details: { field } });
}
return value.trim();
}
function validate(definition) {
const id = text(definition?.id, 'id');
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) throw new TorchError(`Invalid schedule id: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
if (!['system', 'session'].includes(definition.lifetime)) throw new TorchError(`Invalid schedule lifetime: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
if (!['read-only', 'mutating'].includes(definition.behavior)) throw new TorchError(`Invalid schedule behavior: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
if (!Array.isArray(definition.required_authority) || !definition.required_authority.length) {
throw new TorchError(`Schedule has no required authority: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
}
if (!definition.trigger || !['manual', 'interval', 'cron'].includes(definition.trigger.type)) {
throw new TorchError(`Invalid schedule trigger: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
}
if (definition.trigger.type === 'interval'
&& (!Number.isInteger(definition.trigger.seconds) || definition.trigger.seconds < 1)) {
throw new TorchError(`Invalid schedule interval: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
}
if (definition.trigger.type === 'cron'
&& (typeof definition.trigger.expression !== 'string' || definition.trigger.expression.trim().split(/\s+/).length !== 5)) {
throw new TorchError(`Invalid schedule cron expression: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
}
if (definition.action?.type !== 'command' || typeof definition.action.command !== 'string'
|| !Array.isArray(definition.action.args) || definition.action.args.some((arg) => typeof arg !== 'string')) {
throw new TorchError(`Schedule action must be a shell-free command: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
}
const attempts = definition.retry?.max_attempts ?? 1;
if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10) {
throw new TorchError(`Invalid schedule retry policy: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' });
}
text(definition.owner, 'owner');
text(definition.source_of_truth, 'source_of_truth');
return { ...definition, retry: { max_attempts: attempts } };
}
function bounded(value) {
const content = value?.toString() ?? '';
return content.length > 65_536 ? `${content.slice(0, 65_536)}\n[truncated]` : content;
}
export class ScheduleService {
constructor({
repositoryRoot, controlPlane,
executor = (command, args, options) => spawnSync(command, args, options),
clock = () => new Date(), idFactory = randomUUID,
} = {}) {
this.repositoryRoot = repositoryRoot;
this.controlPlane = controlPlane;
this.executor = executor;
this.clock = clock;
this.idFactory = idFactory;
const { config } = loadFleetDefinition(repositoryRoot);
this.definitions = new Map((config.schedules ?? []).map((definition) => {
const normalized = validate(definition);
return [normalized.id, normalized];
}));
}
#initialize() {
this.controlPlane.database.exec(`
CREATE TABLE IF NOT EXISTS schedule_runs (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
schedule_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
started_at TEXT NOT NULL,
finished_at TEXT NOT NULL,
attempts INTEGER NOT NULL,
exit_status INTEGER,
result TEXT NOT NULL,
stdout TEXT,
stderr TEXT
);
CREATE INDEX IF NOT EXISTS schedule_runs_schedule_time
ON schedule_runs(schedule_id, started_at, id);
`);
}
#definition(id) {
const definition = this.definitions.get(id);
if (!definition) throw new TorchError(`Unknown schedule: ${id}`, { code: 'SCHEDULE_NOT_FOUND' });
return definition;
}
runs({ scheduleId, limit = 100 } = {}) {
const table = this.controlPlane.database.prepare(`
SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schedule_runs'
`).get();
if (!table) return [];
const safeLimit = Number.isInteger(limit) && limit > 0 && limit <= 1000 ? limit : 100;
const rows = scheduleId
? this.controlPlane.database.prepare(`SELECT * FROM schedule_runs WHERE schedule_id = ? ORDER BY started_at DESC, id DESC LIMIT ?`).all(scheduleId, safeLimit)
: this.controlPlane.database.prepare(`SELECT * FROM schedule_runs ORDER BY started_at DESC, id DESC LIMIT ?`).all(safeLimit);
return rows.map((row) => ({
id: row.id, scheduleId: row.schedule_id, actorId: row.actor_id,
startedAt: row.started_at, finishedAt: row.finished_at, attempts: row.attempts,
exitStatus: row.exit_status, result: row.result, stdout: row.stdout, stderr: row.stderr,
}));
}
plan({ scheduleId, actorId, at = this.clock() } = {}) {
const definition = this.#definition(scheduleId);
const blockers = [];
if (!definition.required_authority.includes(actorId)) blockers.push('actor-not-authorized');
if (actorId !== 'owner') {
try { this.controlPlane.assertIdentity(actorId); } catch { blockers.push('actor-not-a-fleet-identity'); }
}
if (definition.lifetime === 'session') {
const manager = this.controlPlane.identity('session-manager');
if (manager.state === 'offline') blockers.push('session-manager-offline');
}
const lastRun = this.runs({ scheduleId, limit: 1 })[0] ?? null;
let due = definition.trigger.type === 'manual' ? null : !lastRun;
let nextAt = null;
if (definition.trigger.type === 'interval' && lastRun) {
nextAt = new Date(Date.parse(lastRun.startedAt) + definition.trigger.seconds * 1000).toISOString();
due = at.getTime() >= Date.parse(nextAt);
}
if (definition.trigger.type === 'cron') due = 'external-launcher';
return {
schedule: definition, actorId, due, nextAt, blockers,
canRun: blockers.length === 0, mutationPerformed: false,
};
}
run({ scheduleId, actorId, approved = false } = {}) {
const plan = this.plan({ scheduleId, actorId });
if (!plan.canRun) throw new TorchError(`Schedule cannot run: ${scheduleId}`, { code: 'SCHEDULE_BLOCKED', details: plan.blockers });
if (plan.schedule.behavior === 'mutating' && approved !== true) {
throw new TorchError('Mutating schedule requires explicit approval', { code: 'APPROVAL_REQUIRED' });
}
const startedAt = this.clock().toISOString();
let execution;
let attempts = 0;
do {
attempts += 1;
execution = this.executor(plan.schedule.action.command, plan.schedule.action.args, {
cwd: this.repositoryRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: false,
});
} while ((execution.status !== 0 || execution.error) && attempts < plan.schedule.retry.max_attempts);
const succeeded = execution.status === 0 && !execution.error && !execution.signal;
const run = {
id: this.idFactory(), scheduleId, actorId, startedAt, finishedAt: this.clock().toISOString(),
attempts, exitStatus: execution.status ?? null, result: succeeded ? 'succeeded' : 'failed',
stdout: bounded(execution.stdout), stderr: bounded(execution.stderr ?? execution.error?.message),
};
this.#initialize();
this.controlPlane.database.prepare(`
INSERT INTO schedule_runs (
id, project_id, schedule_id, actor_id, started_at, finished_at,
attempts, exit_status, result, stdout, stderr
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
run.id, this.controlPlane.projectId, run.scheduleId, run.actorId, run.startedAt,
run.finishedAt, run.attempts, run.exitStatus, run.result, run.stdout, run.stderr,
);
if (!succeeded && plan.schedule.failure_recipient && actorId !== 'owner') {
this.controlPlane.sendMessage({
sender: actorId, recipient: plan.schedule.failure_recipient, kind: 'blocker',
body: `Schedule ${scheduleId} failed after ${attempts} attempt${attempts === 1 ? '' : 's'}.`,
});
}
return { ...run, mutationPerformed: true };
}
list({ actorId } = {}) {
return [...this.definitions.values()].map((definition) => this.plan({ scheduleId: definition.id, actorId }));
}
}
+1
View File
@@ -22,6 +22,7 @@ export const CANDIDATE_ACCEPTANCE_SCENARIOS = Object.freeze([
'detach-uninstall',
'combatrig-compatibility',
'product-surface',
'operations-observability',
]);
function json(path) {
+141
View File
@@ -0,0 +1,141 @@
import { randomUUID } from 'node:crypto';
import { TorchError } from '../kernel/errors.mjs';
const METRICS = [
'cachedInput', 'uncachedInput', 'cacheCreation', 'cacheRead',
'compactions', 'resumedPromptBytes', 'costMicrousd', 'verifiedItems',
];
function nonnegativeInteger(value, field) {
const number = value === undefined || value === null || value === '' ? 0 : Number(value);
if (!Number.isSafeInteger(number) || number < 0) {
throw new TorchError(`${field} must be a nonnegative safe integer`, {
code: 'CONTEXT_METRIC_INVALID', details: { field, value },
});
}
return number;
}
function optionalText(value) {
return typeof value === 'string' && value.trim() ? value.trim() : null;
}
function hasTable(database) {
return Boolean(database.prepare(`
SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'context_usage'
`).get());
}
export class ContextTelemetryService {
constructor({ controlPlane, clock = () => new Date(), idFactory = randomUUID } = {}) {
this.controlPlane = controlPlane;
this.clock = clock;
this.idFactory = idFactory;
}
#initialize() {
this.controlPlane.database.exec(`
CREATE TABLE IF NOT EXISTS context_usage (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
area_id TEXT NOT NULL,
runtime TEXT,
runtime_session_id TEXT,
task_ref TEXT,
commit_sha TEXT,
measurement TEXT NOT NULL,
source TEXT NOT NULL,
cached_input INTEGER NOT NULL,
uncached_input INTEGER NOT NULL,
cache_creation INTEGER NOT NULL,
cache_read INTEGER NOT NULL,
compactions INTEGER NOT NULL,
resumed_prompt_bytes INTEGER NOT NULL,
cost_microusd INTEGER NOT NULL,
verified_items INTEGER NOT NULL,
recorded_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS context_usage_area_time
ON context_usage(area_id, recorded_at, id);
CREATE INDEX IF NOT EXISTS context_usage_task
ON context_usage(task_ref, area_id);
`);
}
record(input = {}) {
const areaId = this.controlPlane.assertIdentity(input.areaId);
if (!['measured', 'estimated'].includes(input.measurement)) {
throw new TorchError('Context usage must declare measured or estimated evidence', {
code: 'CONTEXT_MEASUREMENT_INVALID', details: { measurement: input.measurement },
});
}
const source = optionalText(input.source);
if (!source) throw new TorchError('Context usage requires an evidence source', { code: 'CONTEXT_SOURCE_REQUIRED' });
const values = Object.fromEntries(METRICS.map((field) => [field, nonnegativeInteger(input[field], field)]));
const sample = {
id: this.idFactory(), projectId: this.controlPlane.projectId, areaId,
runtime: optionalText(input.runtime), runtimeSessionId: optionalText(input.runtimeSessionId),
task: optionalText(input.task), commit: optionalText(input.commit),
measurement: input.measurement, source, ...values, recordedAt: this.clock().toISOString(),
};
this.#initialize();
this.controlPlane.database.prepare(`
INSERT INTO context_usage (
id, project_id, area_id, runtime, runtime_session_id, task_ref, commit_sha,
measurement, source, cached_input, uncached_input, cache_creation, cache_read,
compactions, resumed_prompt_bytes, cost_microusd, verified_items, recorded_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
sample.id, sample.projectId, sample.areaId, sample.runtime, sample.runtimeSessionId,
sample.task, sample.commit, sample.measurement, sample.source, sample.cachedInput,
sample.uncachedInput, sample.cacheCreation, sample.cacheRead, sample.compactions,
sample.resumedPromptBytes, sample.costMicrousd, sample.verifiedItems, sample.recordedAt,
);
this.controlPlane.audit({
actorId: areaId, operation: 'context.record', entityType: 'context-usage', entityId: sample.id,
details: { measurement: sample.measurement, source: sample.source, task: sample.task, commit: sample.commit },
});
return sample;
}
report({ areaId, task, commit, measurement } = {}) {
if (areaId) this.controlPlane.assertIdentity(areaId);
if (measurement && !['measured', 'estimated'].includes(measurement)) {
throw new TorchError('Unknown context measurement class', { code: 'CONTEXT_MEASUREMENT_INVALID' });
}
if (!hasTable(this.controlPlane.database)) {
return { measured: false, samples: 0, groups: [], warning: 'No context usage has been reported.' };
}
const clauses = [];
const parameters = [];
for (const [column, value] of [['area_id', areaId], ['task_ref', task], ['commit_sha', commit], ['measurement', measurement]]) {
if (value) { clauses.push(`${column} = ?`); parameters.push(value); }
}
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
const groups = this.controlPlane.database.prepare(`
SELECT area_id AS areaId, measurement,
COUNT(*) AS samples,
SUM(cached_input) AS cachedInput,
SUM(uncached_input) AS uncachedInput,
SUM(cache_creation) AS cacheCreation,
SUM(cache_read) AS cacheRead,
SUM(compactions) AS compactions,
SUM(resumed_prompt_bytes) AS resumedPromptBytes,
SUM(cost_microusd) AS costMicrousd,
SUM(verified_items) AS verifiedItems
FROM context_usage ${where}
GROUP BY area_id, measurement ORDER BY area_id, measurement
`).all(...parameters).map((group) => ({
...group,
cacheReadRatio: group.cacheRead + group.uncachedInput > 0
? group.cacheRead / (group.cacheRead + group.uncachedInput) : null,
costPerVerifiedItemMicrousd: group.verifiedItems > 0 ? group.costMicrousd / group.verifiedItems : null,
}));
return {
measured: groups.some((group) => group.measurement === 'measured'),
samples: groups.reduce((total, group) => total + group.samples, 0), groups,
warning: groups.some((group) => group.verifiedItems === 0)
? 'Some usage has no verified outcome; lower context volume alone is not success.' : null,
};
}
}
+1
View File
@@ -38,6 +38,7 @@ function releaseCandidate(root, version) {
'SCN-fleet-fresh-resume', 'SCN-install-doctor-purge',
'SCN-combatrig-import', 'SCN-cli-combatrig-import',
'SCN-product-site', 'SCN-console-readonly',
'SCN-context-locality', 'SCN-schedule-boundaries', 'SCN-cli-schedules',
].join('\n'))});\n`);
writeFileSync(join(candidateRoot, 'package.json'), `${JSON.stringify({
name: 'torch-agent-fleet', version, bin: { torch: 'bin/torch.mjs' },
+75
View File
@@ -0,0 +1,75 @@
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { callTorchTool } from '../../src/mcp/tools.mjs';
import { observeProject } from '../../src/observability/snapshot.mjs';
import { ContextTelemetryService } from '../../src/telemetry/context.mjs';
const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname;
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-context-'));
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.review = {
status: 'approved', reviewedAt: '2026-09-27T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
};
const env = { ...process.env, XDG_DATA_HOME: mkdtempSync(join(tmpdir(), 'torch-context-state-')) };
installProject({ repository, proposal, env, projectId: 'context-fixture' });
return { root, env, worker: proposal.domains[0].id };
}
test('SCN-context-locality: usage evidence stays identity-bound, separates estimates, and prices verified outcomes', () => {
const context = fixture();
const recorded = spawnSync(process.execPath, [
CLI, 'context', 'record', '--area', context.worker, '--source', 'provider-response',
'--measurement', 'measured', '--runtime', 'codex', '--session', 'thread-1', '--task', 'TASK-1',
'--cached-input', '800', '--uncached-input', '200', '--cache-read', '800',
'--cost-microusd', '500000', '--verified-items', '2', '--json',
], { cwd: context.root, env: context.env, encoding: 'utf8' });
assert.equal(recorded.status, 0, recorded.stderr || recorded.stdout);
assert.equal(JSON.parse(recorded.stdout).measurement, 'measured');
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
const telemetry = new ContextTelemetryService({
controlPlane: control, idFactory: () => 'estimated-sample',
clock: () => new Date('2026-09-27T01:00:00Z'),
});
const estimated = callTorchTool(control, 'torch_record_context_usage', {
source: 'prompt-size-heuristic', measurement: 'estimated', cached_input: 100,
uncached_input: 400, cost_microusd: 100000, verified_items: 0,
}, { actorId: context.worker, contextTelemetryService: telemetry });
assert.equal(estimated.areaId, context.worker);
assert.throws(() => callTorchTool(control, 'torch_record_context_usage', {
area_id: 'session-manager', source: 'forged', measurement: 'measured',
}, { actorId: context.worker, contextTelemetryService: telemetry }),
(error) => error.code === 'FLEET_IDENTITY_MISMATCH');
const report = telemetry.report({ areaId: context.worker });
assert.equal(report.measured, true);
assert.equal(report.samples, 2);
assert.deepEqual(report.groups.map((group) => group.measurement), ['estimated', 'measured']);
const measured = report.groups.find((group) => group.measurement === 'measured');
assert.equal(measured.cacheReadRatio, 0.8);
assert.equal(measured.costPerVerifiedItemMicrousd, 250000);
assert.match(report.warning, /no verified outcome/i);
control.close();
const snapshot = observeProject({ repositoryRoot: context.root, env: context.env });
assert.equal(snapshot.contextLocality.measured, true);
assert.deepEqual(snapshot.contextLocality.samples.map((sample) => sample.measurement), ['estimated', 'measured']);
});
+105
View File
@@ -0,0 +1,105 @@
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { ScheduleService } from '../../src/schedules/service.mjs';
const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname;
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-schedules-'));
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
const worker = proposal.domains[0].id;
proposal.schedules = [
{
id: 'fleet-hygiene', title: 'Fleet hygiene', owner: 'session-manager', lifetime: 'session',
trigger: { type: 'interval', seconds: 900 }, behavior: 'read-only',
action: { type: 'command', command: process.execPath, args: ['-e', 'process.stdout.write("healthy")'] },
required_authority: ['session-manager'], retry: { max_attempts: 1 },
failure_recipient: 'session-manager', source_of_truth: 'fixture proposal',
},
{
id: 'domain-audit', title: 'Domain audit', owner: worker, lifetime: 'session',
trigger: { type: 'manual' }, behavior: 'read-only',
action: { type: 'command', command: 'audit-fixture', args: [] },
required_authority: [worker], retry: { max_attempts: 2 },
failure_recipient: 'session-manager', source_of_truth: 'fixture proposal',
},
{
id: 'release', title: 'Release', owner: 'owner', lifetime: 'system',
trigger: { type: 'cron', expression: '0 8,16 * * *' }, behavior: 'mutating',
action: { type: 'command', command: 'release-fixture', args: [] },
required_authority: ['owner'], retry: { max_attempts: 1 },
failure_recipient: 'session-manager', source_of_truth: 'fixture proposal',
},
];
proposal.review = {
status: 'approved', reviewedAt: '2026-09-27T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
};
const env = { ...process.env, XDG_DATA_HOME: mkdtempSync(join(tmpdir(), 'torch-schedules-state-')) };
installProject({ repository, proposal, env, projectId: 'schedule-fixture' });
return { root, env, worker };
}
test('SCN-schedule-boundaries: validated lifetimes, authority, retries, and approval govern shell-free actions', () => {
const context = fixture();
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
const calls = [];
const service = new ScheduleService({
repositoryRoot: context.root, controlPlane: control,
clock: () => new Date('2026-09-27T12:00:00Z'), idFactory: (() => { let id = 0; return () => `run-${++id}`; })(),
executor: (command, args, options) => {
calls.push({ command, args, options });
return command === 'audit-fixture'
? { status: 1, stdout: '', stderr: 'audit failed' }
: { status: 0, stdout: 'ok', stderr: '' };
},
});
assert.deepEqual(service.plan({ scheduleId: 'fleet-hygiene', actorId: 'session-manager' }).blockers, ['session-manager-offline']);
control.reportStatus({ areaId: 'session-manager', state: 'idle', summary: 'Scheduling.' });
const hygiene = service.run({ scheduleId: 'fleet-hygiene', actorId: 'session-manager' });
assert.equal(hygiene.result, 'succeeded');
assert.equal(calls[0].options.shell, false);
const failed = service.run({ scheduleId: 'domain-audit', actorId: context.worker });
assert.equal(failed.result, 'failed');
assert.equal(failed.attempts, 2);
assert.equal(control.readMessages({ recipient: 'session-manager' }).at(-1).kind, 'blocker');
assert.throws(() => service.run({ scheduleId: 'release', actorId: 'owner' }),
(error) => error.code === 'APPROVAL_REQUIRED');
assert.equal(service.run({ scheduleId: 'release', actorId: 'owner', approved: true }).result, 'succeeded');
assert.equal(service.runs({}).length, 3);
control.close();
});
test('SCN-cli-schedules: public commands list, plan, and run a read-only session schedule', () => {
const context = fixture();
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
control.reportStatus({ areaId: 'session-manager', state: 'idle', summary: 'Ready.' });
control.close();
const run = (args) => spawnSync(process.execPath, [CLI, ...args, '--json'], {
cwd: context.root, env: context.env, encoding: 'utf8',
});
const listed = run(['schedules', 'list', '--actor', 'session-manager']);
assert.equal(listed.status, 0, listed.stderr || listed.stdout);
assert.equal(JSON.parse(listed.stdout).schedules.length, 3);
const planned = JSON.parse(run(['schedules', 'plan', '--id', 'fleet-hygiene', '--actor', 'session-manager']).stdout);
assert.equal(planned.canRun, true);
const executed = run(['schedules', 'run', '--id', 'fleet-hygiene', '--actor', 'session-manager']);
assert.equal(executed.status, 0, executed.stderr || executed.stdout);
assert.equal(JSON.parse(executed.stdout).stdout, 'healthy');
});