* feat: add GitHub Copilot CLI as a new provider Add support for GitHub Copilot CLI (@github/copilot) as a takt provider, enabling the 'copilot' command to be used for AI-driven task execution. New files: - src/infra/copilot/client.ts: CLI client with streaming, session ID extraction via --share, and permission mode mapping - src/infra/copilot/types.ts: CopilotCallOptions type definitions - src/infra/copilot/index.ts: barrel exports - src/infra/providers/copilot.ts: CopilotProvider implementing Provider - src/__tests__/copilot-client.test.ts: 20 unit tests for client - src/__tests__/copilot-provider.test.ts: 8 unit tests for provider Key features: - Spawns 'copilot -p' in non-interactive mode with --silent --no-color - Permission modes: full (--yolo), edit (--allow-all-tools --no-ask-user), readonly (no permission flags) - Session ID extraction from --share transcript files - Real-time stdout streaming via onStream callbacks - Configurable via COPILOT_CLI_PATH and COPILOT_GITHUB_TOKEN env vars * fix: remove unused COPILOT_DEFAULT_MAX_AUTOPILOT_CONTINUES constant * fix: address review feedback for copilot provider - Remove excess maxAutopilotContinues property from test (#1 High) - Extract cleanupTmpDir() helper to eliminate DRY violation (#2 Medium) - Deduplicate chunk string conversion in stdout handler (#3 Medium) - Remove 5 what/how comments that restate code (#4 Medium) - Log readFile failure instead of silently swallowing (#5 Medium) - Add credential scrubbing (ghp_/ghs_/gho_/github_pat_) for stderr (#6 Medium) - Add buffer overflow tests for stdout and stderr (#7 Medium) - Add pre-aborted AbortSignal test (#8 Low) - Add mkdtemp failure fallback test (#9 Low) - Add rm cleanup verification to fallback test (#10 Low) - Log mkdtemp failure with debug level (#11 Persist) - Add createLogger('copilot-client') for structured logging
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import type { PermissionMode } from '../models/types.js';
|
|
import type { ProviderPermissionProfiles, ProviderProfileName } from '../models/provider-profiles.js';
|
|
|
|
export interface ResolvePermissionModeInput {
|
|
movementName: string;
|
|
requiredPermissionMode?: PermissionMode;
|
|
provider?: ProviderProfileName;
|
|
projectProviderProfiles?: ProviderPermissionProfiles;
|
|
globalProviderProfiles?: ProviderPermissionProfiles;
|
|
}
|
|
|
|
export const DEFAULT_PROVIDER_PERMISSION_PROFILES: ProviderPermissionProfiles = {
|
|
claude: { defaultPermissionMode: 'edit' },
|
|
codex: { defaultPermissionMode: 'edit' },
|
|
opencode: { defaultPermissionMode: 'edit' },
|
|
cursor: { defaultPermissionMode: 'edit' },
|
|
copilot: { defaultPermissionMode: 'edit' },
|
|
mock: { defaultPermissionMode: 'edit' },
|
|
};
|
|
|
|
/**
|
|
* Resolve movement permission mode using provider profiles.
|
|
*
|
|
* Priority:
|
|
* 1. project provider_profiles.<provider>.movement_permission_overrides.<movement>
|
|
* 2. global provider_profiles.<provider>.movement_permission_overrides.<movement>
|
|
* 3. project provider_profiles.<provider>.default_permission_mode
|
|
* 4. global provider_profiles.<provider>.default_permission_mode
|
|
* 5. apply movement.required_permission_mode as minimum floor
|
|
*
|
|
* Throws when unresolved.
|
|
*/
|
|
export function resolveMovementPermissionMode(input: ResolvePermissionModeInput): PermissionMode {
|
|
if (!input.provider) {
|
|
return input.requiredPermissionMode ?? 'readonly';
|
|
}
|
|
|
|
const projectProfile = input.projectProviderProfiles?.[input.provider];
|
|
const globalProfile = input.globalProviderProfiles?.[input.provider];
|
|
|
|
const projectOverride = projectProfile?.movementPermissionOverrides?.[input.movementName];
|
|
if (projectOverride) {
|
|
return applyRequiredPermissionFloor(projectOverride, input.requiredPermissionMode);
|
|
}
|
|
|
|
const globalOverride = globalProfile?.movementPermissionOverrides?.[input.movementName];
|
|
if (globalOverride) {
|
|
return applyRequiredPermissionFloor(globalOverride, input.requiredPermissionMode);
|
|
}
|
|
|
|
if (projectProfile?.defaultPermissionMode) {
|
|
return applyRequiredPermissionFloor(projectProfile.defaultPermissionMode, input.requiredPermissionMode);
|
|
}
|
|
|
|
if (globalProfile?.defaultPermissionMode) {
|
|
return applyRequiredPermissionFloor(globalProfile.defaultPermissionMode, input.requiredPermissionMode);
|
|
}
|
|
|
|
if (input.requiredPermissionMode) {
|
|
return input.requiredPermissionMode;
|
|
}
|
|
|
|
return 'readonly';
|
|
}
|
|
|
|
const PERMISSION_MODE_RANK: Record<PermissionMode, number> = {
|
|
readonly: 0,
|
|
edit: 1,
|
|
full: 2,
|
|
};
|
|
|
|
function applyRequiredPermissionFloor(
|
|
resolvedMode: PermissionMode,
|
|
requiredMode?: PermissionMode,
|
|
): PermissionMode {
|
|
if (!requiredMode) {
|
|
return resolvedMode;
|
|
}
|
|
return PERMISSION_MODE_RANK[requiredMode] > PERMISSION_MODE_RANK[resolvedMode]
|
|
? requiredMode
|
|
: resolvedMode;
|
|
}
|