mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 04:42:24 +00:00
Add schedules and context locality telemetry
This commit is contained in:
1 parent
d2837581d9
commit
55a4981567
17 files changed
+729
-15
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export const CANDIDATE_ACCEPTANCE_SCENARIOS = Object.freeze([
|
||||
'detach-uninstall',
|
||||
'combatrig-compatibility',
|
||||
'product-surface',
|
||||
'operations-observability',
|
||||
]);
|
||||
|
||||
function json(path) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user