Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/commands/airc/send/browser/AircSendBrowserCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
*/

import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase';
import type { JTAGContext } from '@system/core/types/JTAGTypes';
import type { CommandScope, JTAGContext } from '@system/core/types/JTAGTypes';
import type { AircSendParams, AircSendResult } from '../shared/AircSendTypes';

export class AircSendBrowserCommand extends CommandBase<AircSendParams, AircSendResult> {
protected static override get naturalScope(): CommandScope {
return { type: 'room' };
}

constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) {
super('airc/send', context, subpath, commander);
Expand Down
5 changes: 4 additions & 1 deletion src/commands/airc/send/server/AircSendServerCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@ import { spawn } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import * as path from 'node:path';
import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase';
import type { JTAGContext } from '@system/core/types/JTAGTypes';
import type { CommandScope, JTAGContext } from '@system/core/types/JTAGTypes';
import { ValidationError } from '@system/core/types/ErrorTypes';
import type { AircSendParams, AircSendResult } from '../shared/AircSendTypes';
import { createAircSendResultFromParams } from '../shared/AircSendTypes';

export class AircSendServerCommand extends CommandBase<AircSendParams, AircSendResult> {
protected static override get naturalScope(): CommandScope {
return { type: 'room' };
}

constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) {
super('airc/send', context, subpath, commander);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export class DecisionProposeServerCommand extends DecisionProposeCommand {

const proposerId: UUID = params.userId;
const proposerName: string = proposerResult.data.displayName;
const scope = params.scope || 'all';
const scope = params.proposalScope || 'all';
const significanceLevel = params.significanceLevel || 'medium';
const proposalId = generateUUID();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface DecisionProposeParams extends CommandParams {
}>;

/** Who should vote on this? */
scope?: ProposalScope; // Default: 'all'
proposalScope?: ProposalScope; // Default: 'all'

/** How urgent is this? Determines response window */
significanceLevel?: SignificanceLevel; // Default: 'medium'
Expand Down Expand Up @@ -102,4 +102,3 @@ export const createCollaborationDecisionProposeResultFromParams = (
params: DecisionProposeParams,
differences: Omit<DecisionProposeResult, 'context' | 'sessionId' | 'userId'>
): DecisionProposeResult => transformPayload(params, differences);

6 changes: 5 additions & 1 deletion src/commands/grid/send/browser/GridSendBrowserCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
*/

import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase';
import type { JTAGContext } from '@system/core/types/JTAGTypes';
import type { CommandScope, JTAGContext } from '@system/core/types/JTAGTypes';
import type { GridSendParams, GridSendResult } from '../shared/GridSendTypes';

export class GridSendBrowserCommand extends CommandBase<GridSendParams, GridSendResult> {
protected static override get naturalScope(): CommandScope {
return { type: 'grid' };
}

constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) {
super('grid/send', context, subpath, commander);
}
Expand Down
6 changes: 5 additions & 1 deletion src/commands/grid/send/server/GridSendServerCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
*/

import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase';
import type { JTAGContext } from '@system/core/types/JTAGTypes';
import type { CommandScope, JTAGContext } from '@system/core/types/JTAGTypes';
import type { GridSendParams, GridSendResult } from '../shared/GridSendTypes';
import { RustCoreIPCClient, getContinuumCoreSocketPath } from '../../../../workers/continuum-core/bindings/RustCoreIPC';

export class GridSendServerCommand extends CommandBase<GridSendParams, GridSendResult> {
private rustClient: RustCoreIPCClient;

protected static override get naturalScope(): CommandScope {
return { type: 'grid' };
}

constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) {
super('grid/send', context, subpath, commander);
this.rustClient = new RustCoreIPCClient(getContinuumCoreSocketPath());
Expand Down
4 changes: 2 additions & 2 deletions src/commands/skill/list/server/SkillListServerCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export class SkillListServerCommand extends CommandBase<SkillListParams, SkillLi
if (params.status?.trim()) {
filter.status = params.status;
}
if (params.scope?.trim()) {
filter.scope = params.scope;
if (params.skillScope?.trim()) {
filter.scope = params.skillScope;
}
if (params.createdById?.trim()) {
filter.createdById = params.createdById;
Expand Down
10 changes: 5 additions & 5 deletions src/commands/skill/list/shared/SkillListTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import type { UUID } from '@system/core/types/CrossPlatformUUID';
export interface SkillListParams extends CommandParams {
// Filter by lifecycle status (proposed, approved, generated, validated, active, failed, deprecated)
status?: string;
// Filter by scope (personal, team)
scope?: string;
// Filter by skill visibility scope (personal, team)
skillScope?: string;
// Filter by creator persona ID
createdById?: string;
// Maximum results to return (default: 20)
Expand All @@ -34,8 +34,8 @@ export const createSkillListParams = (
data: {
// Filter by lifecycle status (proposed, approved, generated, validated, active, failed, deprecated)
status?: string;
// Filter by scope (personal, team)
scope?: string;
// Filter by skill visibility scope (personal, team)
skillScope?: string;
// Filter by creator persona ID
createdById?: string;
// Maximum results to return (default: 20)
Expand All @@ -44,7 +44,7 @@ export const createSkillListParams = (
): SkillListParams => createPayload(context, sessionId, {
userId: SYSTEM_SCOPES.SYSTEM,
status: data.status ?? '',
scope: data.scope ?? '',
skillScope: data.skillScope ?? '',
createdById: data.createdById ?? '',
limit: data.limit ?? 0,
...data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class SkillProposeServerCommand extends CommandBase<SkillProposeParams, S

async execute(params: SkillProposeParams): Promise<SkillProposeResult> {
const { name, description, implementation, personaId } = params;
const scope: SkillScope = (params.scope === 'team' ? 'team' : 'personal');
const scope: SkillScope = (params.skillScope === 'team' ? 'team' : 'personal');

if (!name?.trim()) {
throw new ValidationError('name', "Missing required parameter 'name'. Provide the command name (e.g., 'analysis/complexity').");
Expand Down Expand Up @@ -99,7 +99,7 @@ export class SkillProposeServerCommand extends CommandBase<SkillProposeParams, S
{ label: 'Request Changes', description: 'Suggest modifications before approval' },
{ label: 'Reject', description: 'Decline this skill proposal' },
],
scope: 'all',
proposalScope: 'all',
significanceLevel: 'medium',
context: proposeContext,
});
Expand Down
6 changes: 3 additions & 3 deletions src/commands/skill/propose/shared/SkillProposeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface SkillProposeParams extends CommandParams {
// Natural language description of the implementation logic
implementation: string;
// Who can use it: 'personal' (default) or 'team' (requires approval)
scope?: string;
skillScope?: string;
// Usage examples array [{description, command, expectedResult?}]
examples?: Record<string, unknown>[];
// AI persona proposing this skill
Expand All @@ -51,15 +51,15 @@ export const createSkillProposeParams = (
// Natural language description of the implementation logic
implementation: string;
// Who can use it: 'personal' (default) or 'team' (requires approval)
scope?: string;
skillScope?: string;
// Usage examples array [{description, command, expectedResult?}]
examples?: Record<string, unknown>[];
// AI persona proposing this skill
personaId: string;
}
): SkillProposeParams => createPayload(context, sessionId, {
userId: SYSTEM_SCOPES.SYSTEM,
scope: data.scope ?? '',
skillScope: data.skillScope ?? '',
examples: data.examples ?? undefined,
...data
});
Expand Down
26 changes: 23 additions & 3 deletions src/daemons/command-daemon/shared/CommandBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { JTAGModule } from '../../../system/core/shared/JTAGModule';
import type { JTAGContext, CommandParams, CommandResult } from '../../../system/core/types/JTAGTypes';
import type { CommandScope, JTAGContext, CommandParams, CommandResult } from '../../../system/core/types/JTAGTypes';
import { JTAG_ENVIRONMENTS, JTAGMessageFactory } from '../../../system/core/types/JTAGTypes';
import { type UUID } from '../../../system/core/types/CrossPlatformUUID';
import { SYSTEM_SCOPES } from '../../../system/core/types/SystemScopes';
Expand Down Expand Up @@ -82,6 +82,17 @@ export abstract class CommandBase<TParams extends CommandParams = CommandParams,
return 'auto';
}

/**
* Natural execution scope for this command.
*
* Subclasses override this when a command is inherently room/project/grid
* scoped. Commands with no natural scope leave params.scope unset unless
* the caller provided one explicitly.
*/
protected static get naturalScope(): CommandScope | undefined {
return undefined;
}

/**
* Static execute - Universal command execution from anywhere
*
Expand Down Expand Up @@ -154,7 +165,16 @@ export abstract class CommandBase<TParams extends CommandParams = CommandParams,
* @param sessionId - Current session ID from the active request
*/
public getDefaultParams(sessionId: UUID, context: JTAGContext): TParams {
return {sessionId, context, userId: SYSTEM_SCOPES.SYSTEM} as TParams;
const commandClass = this.constructor as typeof CommandBase;
const params: CommandParams = {
sessionId,
context,
userId: SYSTEM_SCOPES.SYSTEM,
};
if (commandClass.naturalScope) {
return { ...params, scope: commandClass.naturalScope } as TParams;
}
return params as TParams;
}

/**
Expand Down Expand Up @@ -292,4 +312,4 @@ export abstract class CommandBase<TParams extends CommandParams = CommandParams,

return baseResult;
}
}
}
17 changes: 11 additions & 6 deletions src/daemons/command-daemon/shared/CommandDaemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,28 @@ export abstract class CommandDaemon extends DaemonBase {
}

try {
// Check if timeout is specified in command params
const timeout = (message.payload as CommandParams).timeout;

// Resolve userId: use payload's userId if present and real, otherwise resolve from session
let resolvedUserId: UUID = (message.payload as CommandParams).userId ?? SYSTEM_SCOPES.SYSTEM;
if (resolvedUserId === SYSTEM_SCOPES.SYSTEM && requestSessionId) {
resolvedUserId = await this.resolveUserIdFromSession(requestSessionId) ?? SYSTEM_SCOPES.SYSTEM;
}

const scopedParams = command.withDefaults(
{ ...message.payload, userId: resolvedUserId } as Partial<CommandParams>,
requestSessionId,
requestContext,
);

// Check if timeout is specified in command params
const timeout = scopedParams.timeout;

// Grid routing: check if this command should execute on a remote node.
// Uses the same interceptor registered on Commands (server-side only).
// Skip for grid/* commands to avoid infinite recursion.
if (!commandName.startsWith('grid/')) {
const interceptor = (Commands as unknown as { _gridInterceptor: { tryRouteRemote: (cmd: string, params: unknown) => Promise<unknown> } | null })._gridInterceptor;
if (interceptor) {
const remoteResult = await interceptor.tryRouteRemote(commandName, message.payload);
const remoteResult = await interceptor.tryRouteRemote(commandName, scopedParams);
if (remoteResult !== null) {
return createCommandSuccessResponse(remoteResult as CommandResult, requestContext, undefined, requestSessionId);
}
Expand All @@ -166,7 +172,7 @@ export abstract class CommandDaemon extends DaemonBase {

// Execute command with session context for dual logging
const executionPromise = globalSessionContext.withSession(requestSessionId, async () => {
return await command.execute({ userId: resolvedUserId, ...message.payload } as CommandParams);
return await command.execute(scopedParams);
});

// Apply timeout if specified
Expand Down Expand Up @@ -302,4 +308,3 @@ export abstract class CommandDaemon extends DaemonBase {
});
}
}

2 changes: 1 addition & 1 deletion src/eslint-baseline.linux.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5432
5431
38 changes: 37 additions & 1 deletion src/system/core/types/JTAGTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,35 @@ export interface JTAGPayload {
readonly sessionId: UUID;
}

/**
* Command execution scope.
*
* Scope is the typed routing/audit boundary for commands. It lets callers and
* command infrastructure describe where work belongs without parsing command
* names, stdout, or ad-hoc params. Recipe rooms, project workspaces, persona
* turns, and grid nodes can all map to this shape.
*/
export type CommandScopeType =
| 'system'
| 'user'
| 'session'
| 'room'
| 'project'
| 'persona'
| 'grid'
| 'resource';

export interface CommandScope {
/** Scope class used by routers/projections for partitioning. */
readonly type: CommandScopeType;

/** Stable scope identifier, such as room id, repo slug, persona id, or node id. */
readonly id?: string;

/** Human-readable label for diagnostics and UI projections. */
readonly label?: string;
}

/**
* Functional factory for creating payloads - eliminates constructor complexity
* Rust-like inheritance: creates payload from source + differences
Expand Down Expand Up @@ -548,6 +577,13 @@ export interface CommandParams extends JTAGPayload {
*/
readonly userId: UUID;

/**
* Typed execution scope for routing, event projection, audit, and work
* alignment. CommandBase injects the command's natural scope when callers
* don't provide one; explicit caller scope wins.
*/
readonly scope?: CommandScope;

/**
* Optional execution timeout in milliseconds.
* If command execution exceeds this timeout, behavior is controlled by onTimeout.
Expand Down Expand Up @@ -609,4 +645,4 @@ export type CommandMessage<T extends CommandParams = CommandParams> = JTAGMessag
/**
* Session and context propagation through explicit payload parameters
* No global state - everything flows through payload chain
*/
*/
Loading