@ai-outfitter/outfitter 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/code/pi-extension/src/outfitter-extension.js +720 -0
- package/dist/agents/AgentAdapter.d.ts +2 -0
- package/dist/agents/AgentLaunch.js +5 -0
- package/dist/agents/AgentLaunch.js.map +1 -1
- package/dist/agents/OutfitterDocs.d.ts +2 -0
- package/dist/agents/OutfitterDocs.js +38 -0
- package/dist/agents/OutfitterDocs.js.map +1 -0
- package/dist/agents/pi/PiAdapter.js +15 -14
- package/dist/agents/pi/PiAdapter.js.map +1 -1
- package/dist/agents/pi/PiSkillSources.d.ts +8 -0
- package/dist/agents/pi/PiSkillSources.js +69 -0
- package/dist/agents/pi/PiSkillSources.js.map +1 -0
- package/dist/cli/OutfitterCli.js +7 -2
- package/dist/cli/OutfitterCli.js.map +1 -1
- package/dist/cli/commands/PiLoginLaunch.js +25 -728
- package/dist/cli/commands/PiLoginLaunch.js.map +1 -1
- package/dist/cli/commands/RunCommand.d.ts +4 -17
- package/dist/cli/commands/RunCommand.js +10 -146
- package/dist/cli/commands/RunCommand.js.map +1 -1
- package/dist/cli/commands/SetupCommand.d.ts +4 -63
- package/dist/cli/commands/SetupCommand.js +13 -673
- package/dist/cli/commands/SetupCommand.js.map +1 -1
- package/dist/cli/commands/run/RunLaunchSummary.d.ts +2 -0
- package/dist/cli/commands/run/RunLaunchSummary.js +35 -0
- package/dist/cli/commands/run/RunLaunchSummary.js.map +1 -0
- package/dist/cli/commands/run/RunProfileResolution.d.ts +37 -0
- package/dist/cli/commands/run/RunProfileResolution.js +115 -0
- package/dist/cli/commands/run/RunProfileResolution.js.map +1 -0
- package/dist/cli/commands/setup/SetupPrompts.d.ts +14 -0
- package/dist/cli/commands/setup/SetupPrompts.js +296 -0
- package/dist/cli/commands/setup/SetupPrompts.js.map +1 -0
- package/dist/cli/commands/setup/SetupSourceImport.d.ts +5 -0
- package/dist/cli/commands/setup/SetupSourceImport.js +177 -0
- package/dist/cli/commands/setup/SetupSourceImport.js.map +1 -0
- package/dist/cli/commands/setup/SetupSourceLaunch.d.ts +4 -0
- package/dist/cli/commands/setup/SetupSourceLaunch.js +65 -0
- package/dist/cli/commands/setup/SetupSourceLaunch.js.map +1 -0
- package/dist/cli/commands/setup/SetupStarterSource.d.ts +21 -0
- package/dist/cli/commands/setup/SetupStarterSource.js +133 -0
- package/dist/cli/commands/setup/SetupStarterSource.js.map +1 -0
- package/dist/cli/commands/setup/SetupTypes.d.ts +91 -0
- package/dist/cli/commands/setup/SetupTypes.js +26 -0
- package/dist/cli/commands/setup/SetupTypes.js.map +1 -0
- package/doc/architecture/state_writeback_strategy.md +350 -0
- package/doc/documentation/README.md +21 -0
- package/doc/documentation/cli.md +74 -0
- package/doc/documentation/concepts.md +54 -0
- package/doc/documentation/first-time-cli-agent-users.md +137 -0
- package/doc/documentation/getting-started.md +49 -0
- package/doc/documentation/iterating-on-profiles.md +109 -0
- package/doc/documentation/profile-repository.md +111 -0
- package/doc/documentation/profiles.md +183 -0
- package/doc/documentation/state.md +204 -0
- package/doc/documentation/support-matrix.md +46 -0
- package/doc/documentation/switching-to-outfitter.md +130 -0
- package/doc/documentation/usecases/engineering.md +114 -0
- package/doc/documentation/usecases/organization-profile-catalog.md +140 -0
- package/doc/documentation/usecases/persona-reviews.md +173 -0
- package/doc/philosophy.md +25 -0
- package/package.json +3 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Resolves and validates setup starter sources: local .outfitter folders, remote git caches, and starter settings.
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import spawn from 'cross-spawn';
|
|
5
|
+
import { createRemoteRepositoryCachePath, normalizeGitUri, redactProfileSourceUriCredentials, } from '../../../profiles/ProfileCache.js';
|
|
6
|
+
import { isValidProfileId } from '../../../profiles/ProfileLoader.js';
|
|
7
|
+
import { createSettingsLoadPlan, loadSettingsFiles } from '../../../settings/SettingsLoader.js';
|
|
8
|
+
import { formatSettingsIssue, } from './SetupTypes.js';
|
|
9
|
+
export const prepareStarterLayout = (homeDirectory, projectDirectory, setupSourceUri, synchronizer = createGitSetupSourceSynchronizer()) => {
|
|
10
|
+
const localOutfitterPath = resolveLocalSetupSourceOutfitterPathFromUri(setupSourceUri, projectDirectory);
|
|
11
|
+
if (localOutfitterPath !== undefined) {
|
|
12
|
+
const settingsPath = join(localOutfitterPath, 'settings.yml');
|
|
13
|
+
validateStarterSettingsIfPresent(existsSync(settingsPath) ? settingsPath : undefined);
|
|
14
|
+
return {
|
|
15
|
+
cachePath: localOutfitterPath,
|
|
16
|
+
settingsPath: existsSync(settingsPath) ? settingsPath : undefined,
|
|
17
|
+
profilesPath: firstExistingPath(join(localOutfitterPath, 'profiles')),
|
|
18
|
+
sourceKind: 'local-live',
|
|
19
|
+
sourceOutfitterPath: localOutfitterPath,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const cachePath = createSetupSourceCachePath(homeDirectory, setupSourceUri);
|
|
23
|
+
synchronizer.sync(setupSourceUri, cachePath);
|
|
24
|
+
const settingsPath = firstExistingPath(join(cachePath, 'settings.yml'), join(cachePath, '.outfitter', 'settings.yml'));
|
|
25
|
+
validateStarterSettingsIfPresent(settingsPath);
|
|
26
|
+
const preferredProfilesPath = settingsPath?.endsWith(join('.outfitter', 'settings.yml'))
|
|
27
|
+
? join(cachePath, '.outfitter', 'profiles')
|
|
28
|
+
: join(cachePath, 'profiles');
|
|
29
|
+
const profilesPath = firstExistingPath(preferredProfilesPath, join(cachePath, 'profiles'), join(cachePath, '.outfitter', 'profiles'));
|
|
30
|
+
return { cachePath, settingsPath, profilesPath, sourceKind: 'remote-cache' };
|
|
31
|
+
};
|
|
32
|
+
const createSetupSourceCachePath = (homeDirectory, setupSourceUri) => createRemoteRepositoryCachePath(homeDirectory, { uri: setupSourceUri });
|
|
33
|
+
const createGitSetupSourceSynchronizer = () => ({
|
|
34
|
+
sync(uri, cachePath) {
|
|
35
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
36
|
+
if (existsSync(cachePath)) {
|
|
37
|
+
runGit(['-C', cachePath, 'pull', '--ff-only'], uri);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
runGit(['clone', '--', normalizeGitUri(uri), cachePath], uri);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const runGit = (args, sensitiveUri) => {
|
|
44
|
+
const result = spawn.sync('git', args, { stdio: 'pipe', encoding: 'utf8' });
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
/* v8 ignore next -- the final fallback only applies if git emits no stdout or stderr. */
|
|
47
|
+
throw new Error(redactSensitiveText((result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim(), sensitiveUri));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const redactSensitiveText = (message, uri) => message
|
|
51
|
+
.split(uri)
|
|
52
|
+
.join(redactProfileSourceUriCredentials(uri))
|
|
53
|
+
.split(normalizeGitUri(uri))
|
|
54
|
+
.join(redactProfileSourceUriCredentials(normalizeGitUri(uri)));
|
|
55
|
+
const firstExistingPath = (...paths) => paths.find((path) => existsSync(path));
|
|
56
|
+
export const validateStarterSettingsIfPresent = (settingsPath) => {
|
|
57
|
+
if (settingsPath === undefined) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: settingsPath }]));
|
|
61
|
+
if (loaded.issues.length > 0) {
|
|
62
|
+
throw new Error(`Cannot setup from invalid starter settings: ${loaded.issues.map(formatSettingsIssue).join('; ')}`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export const readStarterDefaultProfileId = (settingsPath) => {
|
|
66
|
+
if (settingsPath === undefined) {
|
|
67
|
+
return 'engineer';
|
|
68
|
+
}
|
|
69
|
+
const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: settingsPath }]));
|
|
70
|
+
return loaded.files[0]?.settings.defaultProfile ?? 'engineer';
|
|
71
|
+
};
|
|
72
|
+
export const readStarterExplicitDefaultProfileId = (settingsPath) => {
|
|
73
|
+
if (settingsPath === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: settingsPath }]));
|
|
77
|
+
return loaded.files[0]?.settings.defaultProfile;
|
|
78
|
+
};
|
|
79
|
+
export const readUserDefaultProfileId = (files) => files.find((file) => file.location.scope === 'user')?.settings.defaultProfile ?? 'engineer';
|
|
80
|
+
export const ensureExistingUserSettingsDefaultProfile = (settingsPath, files, defaultProfileId) => {
|
|
81
|
+
const userSettings = files.find((file) => file.location.scope === 'user');
|
|
82
|
+
if (userSettings === undefined || userSettings.settings.defaultProfile !== undefined) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const content = readFileSync(settingsPath, 'utf8');
|
|
86
|
+
writeFileSync(settingsPath, `${content}\ndefault_profile: ${defaultProfileId}\n`);
|
|
87
|
+
};
|
|
88
|
+
export const assertValidDefaultProfileId = (profileId) => {
|
|
89
|
+
if (!isValidProfileId(profileId)) {
|
|
90
|
+
throw new Error(`Default profile '${profileId}' is not a filesystem-safe Outfitter profile id.`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
export const createDefaultSettingsContent = (defaultProfileId = 'engineer') => [
|
|
94
|
+
`default_profile: ${defaultProfileId}`,
|
|
95
|
+
'profile_sources:',
|
|
96
|
+
' - github: ai-outfitter/default-profiles',
|
|
97
|
+
' path: profiles',
|
|
98
|
+
' - path: ./profiles',
|
|
99
|
+
'',
|
|
100
|
+
].join('\n');
|
|
101
|
+
export const createInitialSettingsIfMissing = (settingsPath, starterSettingsPath) => {
|
|
102
|
+
if (existsSync(settingsPath)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
106
|
+
writeFileSync(settingsPath, starterSettingsPath === undefined
|
|
107
|
+
? createDefaultSettingsContent()
|
|
108
|
+
: readStarterSettingsContent(starterSettingsPath));
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
export const readStarterSettingsContent = (starterSettingsPath) => {
|
|
112
|
+
const content = readFileSync(starterSettingsPath, 'utf8');
|
|
113
|
+
const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: starterSettingsPath }]));
|
|
114
|
+
if (loaded.files[0]?.settings.defaultProfile !== undefined) {
|
|
115
|
+
return content;
|
|
116
|
+
}
|
|
117
|
+
return `default_profile: engineer\n${content}`;
|
|
118
|
+
};
|
|
119
|
+
/* v8 ignore start -- local setup-source path probing is covered by filesystem integration tests. */
|
|
120
|
+
export const resolveLocalSetupSourceOutfitterPath = (input) => input.setupSourceUri === undefined
|
|
121
|
+
? undefined
|
|
122
|
+
: resolveLocalSetupSourceOutfitterPathFromUri(input.setupSourceUri, input.projectDirectory);
|
|
123
|
+
export const resolveLocalSetupSourceOutfitterPathFromUri = (setupSourceUri, projectDirectory) => {
|
|
124
|
+
if (isRemoteSetupSourceUri(setupSourceUri)) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const sourcePath = isAbsolute(setupSourceUri) ? setupSourceUri : resolve(projectDirectory, setupSourceUri);
|
|
128
|
+
const outfitterPath = sourcePath.endsWith('.outfitter') ? sourcePath : join(sourcePath, '.outfitter');
|
|
129
|
+
return existsSync(outfitterPath) ? outfitterPath : undefined;
|
|
130
|
+
};
|
|
131
|
+
const isRemoteSetupSourceUri = (source) => /^[a-z][a-z0-9+.-]*:/iu.test(source) && !isAbsolute(source);
|
|
132
|
+
/* v8 ignore stop */
|
|
133
|
+
//# sourceMappingURL=SetupStarterSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SetupStarterSource.js","sourceRoot":"","sources":["../../../../src/cli/commands/setup/SetupStarterSource.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { SyncCommandDependencies, SyncCommandResult } from '../SyncCommand.js';
|
|
2
|
+
import type { WelcomeCommandDependencies, WelcomeCommandResult } from '../WelcomeCommand.js';
|
|
3
|
+
export interface SetupCommandInput {
|
|
4
|
+
readonly homeDirectory: string;
|
|
5
|
+
readonly projectDirectory: string;
|
|
6
|
+
readonly setupSourceUri?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SetupCommandResult {
|
|
9
|
+
readonly settingsPath: string;
|
|
10
|
+
readonly defaultProfilePath: string;
|
|
11
|
+
readonly createdSettings: boolean;
|
|
12
|
+
readonly copiedStarterProfileFiles: number;
|
|
13
|
+
readonly createdDefaultProfile: boolean;
|
|
14
|
+
readonly syncResult: SyncCommandResult;
|
|
15
|
+
readonly welcomeResult?: WelcomeCommandResult;
|
|
16
|
+
readonly messages: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export interface SetupSourceSynchronizer {
|
|
19
|
+
sync(uri: string, cachePath: string): void;
|
|
20
|
+
}
|
|
21
|
+
export interface SetupSourceLaunchInput {
|
|
22
|
+
readonly homeDirectory: string;
|
|
23
|
+
readonly projectDirectory: string;
|
|
24
|
+
readonly profileId?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SetupPiOnboardingLaunchInput {
|
|
27
|
+
readonly homeDirectory: string;
|
|
28
|
+
readonly projectDirectory: string;
|
|
29
|
+
readonly setupSourceUri?: string;
|
|
30
|
+
}
|
|
31
|
+
export type SetupSourcePostImportAction = 'start' | 'exit';
|
|
32
|
+
export type SetupSourcePostImportLaunchTarget = 'selected' | 'default';
|
|
33
|
+
export type SetupCommandDependencies = SyncCommandDependencies & WelcomeCommandDependencies & {
|
|
34
|
+
readonly setupSourceSynchronizer?: SetupSourceSynchronizer;
|
|
35
|
+
readonly selectDefaultProfile?: (profiles: readonly SetupProfileChoice[], currentDefault: string) => Promise<string>;
|
|
36
|
+
readonly selectSetupSourceImportTarget?: (choices: readonly SetupSourceImportTargetChoice[], defaultTarget: SetupSourceImportTarget) => Promise<SetupSourceImportTarget>;
|
|
37
|
+
readonly selectSetupSourceLaunchAction?: (profileId: string, launchTarget: SetupSourcePostImportLaunchTarget) => Promise<SetupSourcePostImportAction>;
|
|
38
|
+
readonly selectSetupSourceImportMode?: (choices: readonly SetupSourceImportModeChoice[], defaultMode: SetupSourceImportMode) => Promise<SetupSourceImportMode>;
|
|
39
|
+
readonly launchSetupSourceProfile?: (input: SetupSourceLaunchInput) => Promise<void>;
|
|
40
|
+
readonly launchPiOnboarding?: (input: SetupPiOnboardingLaunchInput) => Promise<{
|
|
41
|
+
readonly exitCode: number;
|
|
42
|
+
}>;
|
|
43
|
+
readonly runWelcome?: (input: SetupCommandInput, dependencies: SetupCommandDependencies) => Promise<WelcomeCommandResult | undefined>;
|
|
44
|
+
};
|
|
45
|
+
export interface SetupProfileChoice {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly label?: string;
|
|
48
|
+
readonly description?: string;
|
|
49
|
+
}
|
|
50
|
+
export type SetupSourceImportTarget = 'home' | 'project';
|
|
51
|
+
export type SetupSourceImportMode = 'copy' | 'symlink';
|
|
52
|
+
export interface SetupSourceImportTargetChoice {
|
|
53
|
+
readonly target: SetupSourceImportTarget;
|
|
54
|
+
readonly label: string;
|
|
55
|
+
readonly description: string;
|
|
56
|
+
}
|
|
57
|
+
export interface SetupSourceImportModeChoice {
|
|
58
|
+
readonly mode: SetupSourceImportMode;
|
|
59
|
+
readonly label: string;
|
|
60
|
+
readonly description: string;
|
|
61
|
+
}
|
|
62
|
+
export declare const setupSourceImportModeChoices: readonly SetupSourceImportModeChoice[];
|
|
63
|
+
export declare const setupSourceImportTargetChoices: readonly SetupSourceImportTargetChoice[];
|
|
64
|
+
export interface StarterLayout {
|
|
65
|
+
readonly cachePath: string;
|
|
66
|
+
readonly settingsPath?: string;
|
|
67
|
+
readonly profilesPath?: string;
|
|
68
|
+
readonly sourceKind: 'local-live' | 'remote-cache';
|
|
69
|
+
readonly sourceOutfitterPath?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface SetupSourceOnboardingResult {
|
|
72
|
+
readonly importTarget: SetupSourceImportTarget;
|
|
73
|
+
readonly selectedProfileId: string;
|
|
74
|
+
readonly importMode: SetupSourceImportMode;
|
|
75
|
+
}
|
|
76
|
+
export interface AppliedSetupSourceImport {
|
|
77
|
+
readonly settingsPath: string;
|
|
78
|
+
readonly settingsDescription: string;
|
|
79
|
+
readonly profilesPath: string;
|
|
80
|
+
readonly createdSettings: boolean;
|
|
81
|
+
readonly copiedStarterProfileFiles: number;
|
|
82
|
+
readonly copiedStarterResourceFiles: number;
|
|
83
|
+
readonly selectedProfileAlreadyExists: boolean;
|
|
84
|
+
readonly selectedProfileConflictMessage?: string;
|
|
85
|
+
readonly symlinkedOutfitter: boolean;
|
|
86
|
+
}
|
|
87
|
+
export declare const formatSettingsIssue: (issue: {
|
|
88
|
+
readonly filePath: string;
|
|
89
|
+
readonly path: string;
|
|
90
|
+
readonly message: string;
|
|
91
|
+
}) => string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const setupSourceImportModeChoices = [
|
|
2
|
+
{
|
|
3
|
+
mode: 'copy',
|
|
4
|
+
label: 'Copy snapshot',
|
|
5
|
+
description: 'copy profiles into the selected .outfitter folder; safest for normal use',
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
mode: 'symlink',
|
|
9
|
+
label: 'Symlink for development',
|
|
10
|
+
description: 'link the selected .outfitter folder to the local source so shared profile edits apply immediately',
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
export const setupSourceImportTargetChoices = [
|
|
14
|
+
{
|
|
15
|
+
target: 'home',
|
|
16
|
+
label: 'User home',
|
|
17
|
+
description: 'install profiles into ~/.outfitter for all repositories on this machine',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
target: 'project',
|
|
21
|
+
label: 'Current project',
|
|
22
|
+
description: 'install profiles into this project .outfitter folder only',
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
export const formatSettingsIssue = (issue) => `${issue.filePath}#${issue.path} ${issue.message}`;
|
|
26
|
+
//# sourceMappingURL=SetupTypes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SetupTypes.js","sourceRoot":"","sources":["../../../../src/cli/commands/setup/SetupTypes.ts"],"names":[],"mappings":"AAyFA,MAAM,CAAC,MAAM,4BAA4B,GAA2C;IAClF;QACE,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE,0EAA0E;KACxF;IACD;QACE,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE,mGAAmG;KACjH;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,8BAA8B,GAA6C;IACtF;QACE,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,yEAAyE;KACvF;IACD;QACE,MAAM,EAAE,SAAS;QACjB,KAAK,EAAE,iBAAiB;QACxB,WAAW,EAAE,2DAA2D;KACzE;CACF,CAAC;AA4BF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,KAInC,EAAU,EAAE,CAAC,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# State Writeback Strategy
|
|
2
|
+
|
|
3
|
+
This document describes Outfitter's current model for handling writes that agent CLIs make inside a composite profile.
|
|
4
|
+
|
|
5
|
+
A composite profile is temporary, but agent CLIs sometimes perform intentionally durable writes, such as logging in, installing plugins, changing settings, or updating MCP configuration.
|
|
6
|
+
Outfitter makes those paths explicit: adapter-declared writable paths are materialized with a resolved `state_persistence` strategy before the child CLI starts, and non-persistent or unknown writes are diagnosed after the child exits.
|
|
7
|
+
|
|
8
|
+
## Functional model
|
|
9
|
+
|
|
10
|
+
Outfitter separates three kinds of files that may exist in a composite profile:
|
|
11
|
+
|
|
12
|
+
1. **Generated runtime files**: files Outfitter assembles from settings, profiles, templates, and adapter rules.
|
|
13
|
+
Outfitter may regenerate these while the child agent is running when their source inputs change.
|
|
14
|
+
2. **Declared state paths**: adapter-known files or directories the agent CLI may update intentionally, such as auth, settings, MCP config, plugins, caches, and sessions.
|
|
15
|
+
3. **Unknown writes**: files or directories the agent creates outside the adapter-declared state paths.
|
|
16
|
+
|
|
17
|
+
Only declared state paths can be made durable automatically.
|
|
18
|
+
Unknown writes are never silently persisted because Outfitter does not know their intended owner, merge rules, or durable destination.
|
|
19
|
+
Generated runtime files and declared state paths are deliberately handled separately so live profile/template updates do not erase or re-baseline agent state changes made during the same run.
|
|
20
|
+
|
|
21
|
+
The user-facing state update lifecycle is:
|
|
22
|
+
|
|
23
|
+
1. **Choose a profile**.
|
|
24
|
+
Profile resolution determines the effective `state_persistence` map using normal profile precedence.
|
|
25
|
+
2. **Resolve adapter defaults**.
|
|
26
|
+
For each path the selected adapter declares, Outfitter uses the profile override when present and otherwise uses the adapter default.
|
|
27
|
+
3. **Prepare the composite profile**.
|
|
28
|
+
Durable paths are connected to a profile-managed or native CLI location; non-durable paths are created as normal temporary composite profile paths.
|
|
29
|
+
4. **Run the agent**.
|
|
30
|
+
The agent CLI reads and writes the composite profile as if it were its normal configuration directory.
|
|
31
|
+
5. **Classify changes after exit**.
|
|
32
|
+
Outfitter checks non-durable declared paths and unknown paths and reports or fails according to their strategies.
|
|
33
|
+
6. **Clean up temporary state**.
|
|
34
|
+
Temporary composite profile contents are discarded; durable symlink targets remain in their profile or native CLI location.
|
|
35
|
+
|
|
36
|
+
## Current behavior
|
|
37
|
+
|
|
38
|
+
- Composite profiles remain temporary and reproducible by default.
|
|
39
|
+
- Outfitter does not do generic post-run copy-back or JSON/YAML merge-back.
|
|
40
|
+
- Persistent state is represented by symlinking a composite profile path to a profile file/directory or to the native CLI fallback path.
|
|
41
|
+
- Adapters may generate a concrete runtime file for a declared state path when they need deterministic launch-time reconciliation.
|
|
42
|
+
For example, the Pi adapter can generate a transformed `settings.json` that removes native `packages` entries already supplied by profile-controlled extensions, and then mark that declared path as `discard` for write detection during the run.
|
|
43
|
+
- Every adapter-declared state path has a resolved strategy before launch: profile overrides win, otherwise the adapter `default_strategy` is used, except for adapter-generated reconciliation files that are intentionally treated as discarded runtime files.
|
|
44
|
+
- Invalid or disallowed profile-requested `state_persistence` strategies fail before launch; adapter-internal reconciliation may still choose a one-run handling strategy for a generated runtime file.
|
|
45
|
+
- Non-persistent `warn` and `error` strategies are checked after the child CLI exits.
|
|
46
|
+
- Unknown writes outside adapter-declared paths are checked with the adapter's `unknown` pseudo-path strategy.
|
|
47
|
+
- `prompt` is reserved for a future interactive/control-plane workflow.
|
|
48
|
+
When accepted by a declaration today, it is treated as a non-persistent diagnostic like `warn`.
|
|
49
|
+
|
|
50
|
+
## Non-goals
|
|
51
|
+
|
|
52
|
+
- Outfitter does not implement generic copy-back from the composite profile to profiles.
|
|
53
|
+
- Outfitter does not implement generic structured merge-back.
|
|
54
|
+
- Outfitter does not silently persist unknown writes.
|
|
55
|
+
|
|
56
|
+
## Profile stack and native fallback
|
|
57
|
+
|
|
58
|
+
State persistence is a normal profile setting.
|
|
59
|
+
Its strategy overrides resolve through the same selected-profile stack as other profile data. When the user provides `--profile X`, only definitions of `X` and its explicit inheritance chain participate; the configured `default_profile` is selected only when no explicit profile is provided.
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
project-local definition of selected profile
|
|
63
|
+
project definition of selected profile
|
|
64
|
+
user definition of selected profile
|
|
65
|
+
URI/cache definitions of selected profile
|
|
66
|
+
explicit inheritance
|
|
67
|
+
Outfitter defaults
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Native CLI state is not represented as an extra profile layer.
|
|
71
|
+
For `symlink` paths without a profile-provided source, the selected adapter resolves a native fallback location directly, such as `~/.pi/agent/...` for most Pi state paths, `~/.claude/...` for most Claude Code state paths, or `<cache_directory>/utilities` for Pi `utilities/` and `bin/`.
|
|
72
|
+
This native fallback is not a base profile: it does not participate in profile inheritance or merge precedence, and it cannot contribute controls or profile YAML.
|
|
73
|
+
Claude Code `projects/` is additionally controlled by `controls.session_directory` or `controls.claude.session_directory` when set.
|
|
74
|
+
|
|
75
|
+
## Path-keyed adapter declarations
|
|
76
|
+
|
|
77
|
+
Adapters declare writable state paths directly, using relative file paths as keys.
|
|
78
|
+
Directory paths use a trailing slash.
|
|
79
|
+
The same key is used for adapter coverage, `state_persistence` overrides, profile resource lookup, native fallback lookup, and composite profile materialization.
|
|
80
|
+
|
|
81
|
+
The Pi adapter currently declares:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
state_paths:
|
|
85
|
+
auth.json:
|
|
86
|
+
default_strategy: symlink
|
|
87
|
+
allowed_strategies: [symlink, error, prompt]
|
|
88
|
+
|
|
89
|
+
settings.json:
|
|
90
|
+
default_strategy: symlink
|
|
91
|
+
allowed_strategies: [symlink, warn, error, prompt]
|
|
92
|
+
note: >-
|
|
93
|
+
When profile-controlled Pi extensions duplicate native settings packages,
|
|
94
|
+
Outfitter may generate a transformed runtime settings.json and treat this
|
|
95
|
+
declared path as discard for that launch. That discard handling is
|
|
96
|
+
adapter-internal; users still cannot request settings.json: discard
|
|
97
|
+
because discard is not listed in allowed_strategies.
|
|
98
|
+
|
|
99
|
+
keybindings.json:
|
|
100
|
+
default_strategy: symlink
|
|
101
|
+
allowed_strategies: [symlink, warn, error, prompt]
|
|
102
|
+
note: >-
|
|
103
|
+
Outfitter generates a transformed runtime keybindings.json so Shift+Tab
|
|
104
|
+
can switch Outfitter modes and Ctrl+Shift+T can cycle Pi thinking levels.
|
|
105
|
+
The generated runtime file is treated as discard for that launch.
|
|
106
|
+
|
|
107
|
+
mcp.json:
|
|
108
|
+
default_strategy: symlink
|
|
109
|
+
allowed_strategies: [symlink, warn, error, prompt]
|
|
110
|
+
|
|
111
|
+
models.json:
|
|
112
|
+
default_strategy: symlink
|
|
113
|
+
allowed_strategies: [symlink, warn, error, prompt]
|
|
114
|
+
|
|
115
|
+
trust.json:
|
|
116
|
+
default_strategy: symlink
|
|
117
|
+
allowed_strategies: [symlink, warn, error, prompt]
|
|
118
|
+
|
|
119
|
+
plugins/:
|
|
120
|
+
default_strategy: symlink
|
|
121
|
+
allowed_strategies: [symlink, discard, warn, error, prompt]
|
|
122
|
+
|
|
123
|
+
cache/:
|
|
124
|
+
default_strategy: symlink
|
|
125
|
+
allowed_strategies: [symlink, discard, warn, error]
|
|
126
|
+
|
|
127
|
+
sessions/:
|
|
128
|
+
default_strategy: symlink
|
|
129
|
+
allowed_strategies: [symlink, discard, warn, error]
|
|
130
|
+
|
|
131
|
+
npm/:
|
|
132
|
+
default_strategy: symlink
|
|
133
|
+
allowed_strategies: [symlink, discard, warn, error]
|
|
134
|
+
|
|
135
|
+
git/:
|
|
136
|
+
default_strategy: symlink
|
|
137
|
+
allowed_strategies: [symlink, discard, warn, error]
|
|
138
|
+
|
|
139
|
+
tmp/:
|
|
140
|
+
default_strategy: symlink
|
|
141
|
+
allowed_strategies: [symlink, discard]
|
|
142
|
+
|
|
143
|
+
utilities/:
|
|
144
|
+
default_strategy: symlink
|
|
145
|
+
allowed_strategies: [symlink, discard, warn, error]
|
|
146
|
+
|
|
147
|
+
bin/:
|
|
148
|
+
default_strategy: symlink
|
|
149
|
+
allowed_strategies: [symlink, discard, warn, error]
|
|
150
|
+
|
|
151
|
+
unknown:
|
|
152
|
+
default_strategy: warn
|
|
153
|
+
allowed_strategies: [discard, warn, error, prompt]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The Claude Code adapter currently declares:
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
state_paths:
|
|
160
|
+
settings.json:
|
|
161
|
+
default_strategy: symlink
|
|
162
|
+
allowed_strategies: [symlink, warn, error, prompt]
|
|
163
|
+
|
|
164
|
+
agents/:
|
|
165
|
+
default_strategy: symlink
|
|
166
|
+
allowed_strategies: [symlink, discard, warn, error, prompt]
|
|
167
|
+
|
|
168
|
+
skills/:
|
|
169
|
+
default_strategy: symlink
|
|
170
|
+
allowed_strategies: [symlink, discard, warn, error, prompt]
|
|
171
|
+
|
|
172
|
+
commands/:
|
|
173
|
+
default_strategy: symlink
|
|
174
|
+
allowed_strategies: [symlink, discard, warn, error, prompt]
|
|
175
|
+
|
|
176
|
+
plugins/:
|
|
177
|
+
default_strategy: symlink
|
|
178
|
+
allowed_strategies: [symlink, discard, warn, error, prompt]
|
|
179
|
+
|
|
180
|
+
projects/:
|
|
181
|
+
default_strategy: symlink
|
|
182
|
+
allowed_strategies: [symlink, discard, warn, error]
|
|
183
|
+
|
|
184
|
+
debug/:
|
|
185
|
+
default_strategy: symlink
|
|
186
|
+
allowed_strategies: [symlink, discard, warn, error]
|
|
187
|
+
|
|
188
|
+
unknown:
|
|
189
|
+
default_strategy: warn
|
|
190
|
+
allowed_strategies: [discard, warn, error, prompt]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Profile layout for state files
|
|
194
|
+
|
|
195
|
+
State files live under the relevant CLI-specific profile folder:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
profiles/
|
|
199
|
+
default/
|
|
200
|
+
profile.yml
|
|
201
|
+
cli_specific/
|
|
202
|
+
pi/
|
|
203
|
+
auth.json
|
|
204
|
+
settings.json
|
|
205
|
+
keybindings.json
|
|
206
|
+
plugins/
|
|
207
|
+
claude/
|
|
208
|
+
settings.json
|
|
209
|
+
skills/
|
|
210
|
+
commands/
|
|
211
|
+
plugins/
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Except for special adapter paths described below, when a selected strategy is `symlink`, Outfitter searches the resolved profile folders from highest to lowest precedence for `cli_specific/<adapter>/<state-path>`.
|
|
215
|
+
If a profile contains the file or directory, Outfitter symlinks the composite profile path to that source.
|
|
216
|
+
|
|
217
|
+
For most Pi paths, if no profile source exists, Outfitter falls back to the corresponding native Pi agent path under `~/.pi/agent`.
|
|
218
|
+
Missing native fallback files/directories are created so the composite profile symlink has a durable destination.
|
|
219
|
+
|
|
220
|
+
Pi `utilities/` and `bin/` are special cache-backed paths: both resolve to `<cache_directory>/utilities` instead of profile or native Pi state.
|
|
221
|
+
This keeps pi-managed helper binaries reusable across temporary composite profiles without treating them as user-editable profile files.
|
|
222
|
+
|
|
223
|
+
For most Claude Code paths, if no profile source exists, Outfitter falls back to the corresponding native Claude Code path under `~/.claude`.
|
|
224
|
+
Claude Code `projects/` is special: `controls.claude.session_directory` overrides generic `controls.session_directory`, and the selected session directory becomes the `projects/` symlink source.
|
|
225
|
+
If neither session-directory control is present, `projects/` falls back to `~/.claude/projects`.
|
|
226
|
+
|
|
227
|
+
## `state_persistence`
|
|
228
|
+
|
|
229
|
+
Profiles may override persistence by mapping adapter-declared paths to strategy names:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
state_persistence:
|
|
233
|
+
auth.json: symlink
|
|
234
|
+
settings.json: symlink
|
|
235
|
+
plugins/: symlink
|
|
236
|
+
cache/: discard
|
|
237
|
+
sessions/: discard
|
|
238
|
+
unknown: warn
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The values are concrete strategy names.
|
|
242
|
+
`state_persistence` only needs overrides; omitted paths use the adapter declaration's `default_strategy`.
|
|
243
|
+
|
|
244
|
+
`state_persistence` is validated by the profile JSON Schema at read boundaries.
|
|
245
|
+
Outfitter also validates the resolved strategy against the adapter declaration before launch.
|
|
246
|
+
|
|
247
|
+
Functional examples:
|
|
248
|
+
|
|
249
|
+
```yaml
|
|
250
|
+
# Persist logins and settings, but make caches and sessions run-local.
|
|
251
|
+
state_persistence:
|
|
252
|
+
auth.json: symlink
|
|
253
|
+
settings.json: symlink
|
|
254
|
+
cache/: discard
|
|
255
|
+
sessions/: discard
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```yaml
|
|
259
|
+
# CI profile: fail if pi changes settings, MCP config, or unknown files.
|
|
260
|
+
state_persistence:
|
|
261
|
+
settings.json: error
|
|
262
|
+
mcp.json: error
|
|
263
|
+
plugins/: error
|
|
264
|
+
unknown: error
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```yaml
|
|
268
|
+
# Exploratory profile: allow plugin experiments but report them after exit.
|
|
269
|
+
state_persistence:
|
|
270
|
+
plugins/: warn
|
|
271
|
+
unknown: warn
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Composite profile materialization
|
|
275
|
+
|
|
276
|
+
Before launch, Outfitter processes each adapter-declared state path:
|
|
277
|
+
|
|
278
|
+
1. Resolve the path's strategy from profile `state_persistence` overrides, then the adapter `default_strategy`.
|
|
279
|
+
2. Validate that the strategy is allowed for that path.
|
|
280
|
+
3. Resolve a source path through the profile hierarchy when the strategy is `symlink`.
|
|
281
|
+
4. Materialize the composite profile path.
|
|
282
|
+
5. Record a baseline fingerprint for non-persistent and unknown write detection.
|
|
283
|
+
|
|
284
|
+
For `symlink`, Outfitter creates a symlink from the composite profile path to the resolved profile or native CLI source.
|
|
285
|
+
|
|
286
|
+
For `discard`, `warn`, `error`, and `prompt`, Outfitter creates normal temporary composite profile paths where needed and observes whether they changed.
|
|
287
|
+
|
|
288
|
+
## Unknown writes
|
|
289
|
+
|
|
290
|
+
The `unknown` pseudo-path controls writes outside adapter-declared paths:
|
|
291
|
+
|
|
292
|
+
```yaml
|
|
293
|
+
state_persistence:
|
|
294
|
+
unknown: warn
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Supported `unknown` strategies are non-persistent only:
|
|
298
|
+
|
|
299
|
+
- `discard`
|
|
300
|
+
- `warn`
|
|
301
|
+
- `error`
|
|
302
|
+
- `prompt`
|
|
303
|
+
|
|
304
|
+
`unknown` does not support `symlink`, because there is no declared durable destination.
|
|
305
|
+
|
|
306
|
+
## Strategy selection guide
|
|
307
|
+
|
|
308
|
+
Use `symlink` when a write is part of durable agent setup, such as logging in, editing native settings, updating MCP config, or installing plugins that should be reused.
|
|
309
|
+
Use `discard` when the data is useful only during the current run, such as cache entries or throwaway sessions.
|
|
310
|
+
Use `warn` when mutation is acceptable but should be visible to the user.
|
|
311
|
+
Use `error` when mutation means the run was not reproducible enough, especially in CI or locked-down project profiles.
|
|
312
|
+
Use `prompt` only as a forward-compatible declaration for future interactive handling.
|
|
313
|
+
|
|
314
|
+
## Strategies
|
|
315
|
+
|
|
316
|
+
### `symlink`
|
|
317
|
+
|
|
318
|
+
Outfitter resolves the state path through the profile hierarchy, then the native CLI fallback, and symlinks that source into the composite profile.
|
|
319
|
+
Persistence happens because the CLI writes through the symlink to an intentional file or directory.
|
|
320
|
+
|
|
321
|
+
### `discard`
|
|
322
|
+
|
|
323
|
+
Writes are allowed in the composite profile and are thrown away when the composite profile is deleted.
|
|
324
|
+
Outfitter does not emit diagnostics for changed `discard` paths.
|
|
325
|
+
|
|
326
|
+
### `warn`
|
|
327
|
+
|
|
328
|
+
Writes are allowed, discarded, and reported after the child exits.
|
|
329
|
+
`--strict` makes these warnings fatal.
|
|
330
|
+
|
|
331
|
+
### `error`
|
|
332
|
+
|
|
333
|
+
Writes are allowed during the child process but cause Outfitter to fail after the child exits if the path changed.
|
|
334
|
+
This is useful for CI and strict reproducibility.
|
|
335
|
+
|
|
336
|
+
### `prompt`
|
|
337
|
+
|
|
338
|
+
`prompt` is reserved for a future interactive/control-plane workflow.
|
|
339
|
+
Current implementations that allow it treat writes as non-persistent diagnostics, equivalent to `warn`, with the strategy name preserved in the message.
|
|
340
|
+
|
|
341
|
+
## Rationale
|
|
342
|
+
|
|
343
|
+
Path-keyed state declarations keep the model simple:
|
|
344
|
+
|
|
345
|
+
- the adapter declares the paths it knows the CLI may write and their default strategies;
|
|
346
|
+
- profiles may provide files at those same paths;
|
|
347
|
+
- the native fallback exposes native CLI files at those same paths;
|
|
348
|
+
- `state_persistence` says what to do with each path.
|
|
349
|
+
|
|
350
|
+
This avoids ambiguous writeback behavior and gives users a clear rule: if a CLI write should persist, configure that composite profile path as `symlink` and provide or accept the profile/native file that should receive the mutation.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Outfitter documentation
|
|
2
|
+
|
|
3
|
+
User-facing Outfitter documentation.
|
|
4
|
+
|
|
5
|
+
- [Getting started](./getting-started.md)
|
|
6
|
+
- [Concepts](./concepts.md)
|
|
7
|
+
- [CLI reference](./cli.md)
|
|
8
|
+
- [First-time CLI agent users](./first-time-cli-agent-users.md)
|
|
9
|
+
- [Switching to Outfitter](./switching-to-outfitter.md)
|
|
10
|
+
- [Profiles](./profiles.md)
|
|
11
|
+
- [Profile repositories](./profile-repository.md)
|
|
12
|
+
- [Iterating on local and worktree profiles](./iterating-on-profiles.md)
|
|
13
|
+
- [Adapter support matrix](./support-matrix.md)
|
|
14
|
+
- [State persistence](./state.md)
|
|
15
|
+
- [Philosophy](../philosophy.md)
|
|
16
|
+
|
|
17
|
+
## Use cases
|
|
18
|
+
|
|
19
|
+
- [Organization profile catalog](./usecases/organization-profile-catalog.md) — Publish shared team roles so new users can start with organization-approved defaults.
|
|
20
|
+
- [Engineering profile catalog](./usecases/engineering.md) — Package coding, platform, and review profiles for repeatable engineering workflows.
|
|
21
|
+
- [Persona reviews](./usecases/persona-reviews.md) — Create customer personas to get feedback on ideas, documentation, and designs.
|