@ai-outfitter/outfitter 0.6.1 → 0.7.1

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.
Files changed (93) hide show
  1. package/LICENSE.md +20 -50
  2. package/README.md +41 -280
  3. package/code/enterprise/LICENSE +35 -0
  4. package/code/enterprise/README.md +7 -0
  5. package/code/enterprise/cli/privateCatalogGate.cjs +134 -0
  6. package/code/enterprise/cli/privateCatalogSettings.cjs +59 -0
  7. package/code/enterprise/pi-extension/privateCatalogOnboarding.js +89 -0
  8. package/code/enterprise/private-catalog-boundary.json +9 -0
  9. package/code/enterprise/privateCatalog.js +24 -0
  10. package/code/enterprise/shared/privateCatalogPolicy.cjs +66 -0
  11. package/dist/agents/AdapterProfileControls.d.ts +2 -2
  12. package/dist/agents/AdapterProfileControls.js +8 -1
  13. package/dist/agents/AdapterProfileControls.js.map +1 -1
  14. package/dist/agents/AgentAdapter.d.ts +11 -0
  15. package/dist/agents/AgentLaunch.d.ts +6 -0
  16. package/dist/agents/AgentLaunch.js +89 -0
  17. package/dist/agents/AgentLaunch.js.map +1 -0
  18. package/dist/agents/claude/ClaudeAdapter.js +18 -3
  19. package/dist/agents/claude/ClaudeAdapter.js.map +1 -1
  20. package/dist/agents/pi/PiAdapter.js +162 -33
  21. package/dist/agents/pi/PiAdapter.js.map +1 -1
  22. package/dist/agents/pi/PiArgs.d.ts +2 -0
  23. package/dist/agents/pi/PiArgs.js +15 -0
  24. package/dist/agents/pi/PiArgs.js.map +1 -0
  25. package/dist/agents/pi/PiExtensionCache.d.ts +4 -0
  26. package/dist/agents/pi/PiExtensionCache.js +105 -0
  27. package/dist/agents/pi/PiExtensionCache.js.map +1 -0
  28. package/dist/cli/commands/PiLoginLaunch.d.ts +9 -2
  29. package/dist/cli/commands/PiLoginLaunch.js +817 -75
  30. package/dist/cli/commands/PiLoginLaunch.js.map +1 -1
  31. package/dist/cli/commands/RunCommand.d.ts +22 -3
  32. package/dist/cli/commands/RunCommand.js +104 -20
  33. package/dist/cli/commands/RunCommand.js.map +1 -1
  34. package/dist/cli/commands/SetupCommand.d.ts +19 -2
  35. package/dist/cli/commands/SetupCommand.js +281 -57
  36. package/dist/cli/commands/SetupCommand.js.map +1 -1
  37. package/dist/cli/commands/SyncCommand.d.ts +19 -1
  38. package/dist/cli/commands/SyncCommand.js +47 -6
  39. package/dist/cli/commands/SyncCommand.js.map +1 -1
  40. package/dist/cli/commands/WelcomeCommand.js +1 -1
  41. package/dist/cli/commands/WelcomeCommand.js.map +1 -1
  42. package/dist/cli/commands/assets/outfitter-ascii.txt +5 -0
  43. package/dist/cli/commands/profile/Command.d.ts +1 -0
  44. package/dist/cli/commands/profile/Command.js +3 -0
  45. package/dist/cli/commands/profile/Command.js.map +1 -1
  46. package/dist/cli/commands/profile/LintCommand.d.ts +19 -0
  47. package/dist/cli/commands/profile/LintCommand.js +123 -0
  48. package/dist/cli/commands/profile/LintCommand.js.map +1 -0
  49. package/dist/cli.js +8 -2
  50. package/dist/cli.js.map +1 -1
  51. package/dist/merge/ArrayMergePolicy.js.map +1 -1
  52. package/dist/merge/SettingsValueMerger.js.map +1 -1
  53. package/dist/profiles/Profile.d.ts +13 -1
  54. package/dist/profiles/Profile.js.map +1 -1
  55. package/dist/profiles/ProfileLoader.d.ts +4 -0
  56. package/dist/profiles/ProfileLoader.js +117 -17
  57. package/dist/profiles/ProfileLoader.js.map +1 -1
  58. package/dist/profiles/ProfileMerger.js +3 -0
  59. package/dist/profiles/ProfileMerger.js.map +1 -1
  60. package/dist/profiles/PromptIncludes.d.ts +32 -0
  61. package/dist/profiles/PromptIncludes.js +147 -0
  62. package/dist/profiles/PromptIncludes.js.map +1 -0
  63. package/dist/prompts/SystemPromptExport.d.ts +16 -0
  64. package/dist/prompts/SystemPromptExport.js +81 -0
  65. package/dist/prompts/SystemPromptExport.js.map +1 -0
  66. package/dist/schemas/profile.schema.json +37 -2
  67. package/dist/schemas/settings.schema.json +23 -0
  68. package/dist/settings/Settings.d.ts +9 -0
  69. package/dist/settings/Settings.js.map +1 -1
  70. package/dist/settings/SettingsLoader.js +5 -0
  71. package/dist/settings/SettingsLoader.js.map +1 -1
  72. package/dist/settings/SettingsMerger.js +11 -0
  73. package/dist/settings/SettingsMerger.js.map +1 -1
  74. package/package.json +7 -11
  75. package/src/schemas/profile.schema.json +37 -2
  76. package/src/schemas/settings.schema.json +23 -0
  77. package/doc/.deepreview +0 -30
  78. package/doc/architecture.md +0 -856
  79. package/doc/controllable-elements.md +0 -162
  80. package/doc/file_structure.md +0 -141
  81. package/doc/integration_test_system.md +0 -214
  82. package/doc/specs/validating_requirements_with_rules.md +0 -55
  83. package/doc/state_writeback_strategy.md +0 -342
  84. package/requirements/OFTR-001-project-foundation.md +0 -53
  85. package/requirements/OFTR-002-settings.md +0 -65
  86. package/requirements/OFTR-003-profiles.md +0 -60
  87. package/requirements/OFTR-004-sync-and-setup.md +0 -67
  88. package/requirements/OFTR-005-run-and-composite-profile.md +0 -60
  89. package/requirements/OFTR-006-agent-adapters.md +0 -66
  90. package/requirements/OFTR-007-controllable-elements.md +0 -32
  91. package/requirements/OFTR-008-requirements-governance.md +0 -42
  92. package/requirements/OFTR-009-release-publishing.md +0 -35
  93. package/requirements/OFTR-010-onboarding-welcome.md +0 -48
@@ -1,856 +0,0 @@
1
- # Outfitter Architecture
2
-
3
- ## Purpose
4
-
5
- Outfitter is a TypeScript CLI that assembles and launches reproducible agent-CLI profiles.
6
- It is generic enough for organizations to define profiles once and run them across multiple agent CLIs, while supporting `pi` first and most deeply, plus Claude Code as an additional supported adapter.
7
-
8
- Formal implementation requirements live in `requirements/`; this document explains the architectural shape behind those requirements.
9
-
10
- > Naming rule: any occurrence of `outfitter` in docs, prompts, examples, or generated text is a typo/autocorrect and MUST be treated as `outfitter`.
11
-
12
- ## Architectural Principles
13
-
14
- 1. **TypeScript first**: production code, tests, schemas, and tooling are centered on TypeScript.
15
- 2. **Pi-native by default**: when uncertain, follow `pi.dev`/pi-coding-agent naming, behavior, and library choices.
16
- 3. **Generic control model**: user-facing profiles describe generic controllable elements, then adapters translate them to agent-specific files, flags, and environment variables.
17
- 4. **Command objects for complexity**: non-trivial CLI commands are implemented as command objects with explicit dependencies and typed inputs/outputs.
18
- 5. **YAML for persisted config**: user-editable persisted config uses `.yml`/`.yaml` rather than JSON so comments are possible.
19
- 6. **JSON Schema for validation**: every YAML file format has a corresponding JSON Schema used wherever the file is read.
20
- 7. **Deterministic merging**: settings and profile layers merge predictably using normal precedence: project-local, project, then user.
21
- 8. **Warn on partial support**: if a profile asks for a control an agent adapter cannot support, Outfitter warns to stderr; `--strict` makes unsupported controls fatal.
22
- 9. **Complete test coverage early**: the project starts with a test framework and a 100% global coverage requirement.
23
- 10. **Complexity limits early**: ESLint is configured immediately with maximum complexity `10`.
24
-
25
- ## Runtime and Tooling Baseline
26
-
27
- - Runtime: Node.js `>=22.19.0`.
28
- - Language: TypeScript.
29
- - Package manager: npm.
30
- This matches the current pi-coding-agent package distribution model and gives Outfitter a conventional `package-lock.json`-based install path.
31
- - CLI framework: Commander `^14`.
32
- Commander is the initial choice because it supports default commands, command aliases, `allowUnknownOption`, pass-through argument collection, and testable parser construction without spawning child processes.
33
- - Test framework: Vitest `^4` with `@vitest/coverage-v8`.
34
- Pi currently uses Vitest, and Vitest is well suited to TypeScript unit tests around command objects and generated launch plans.
35
- - Coverage: 100% global threshold for statements, branches, functions, and lines from the first implementation.
36
- - Linting: ESLint `^10`, `@eslint/js`, and `typescript-eslint`, with `complexity: ["error", 10]`.
37
- - Schema and validation: TypeBox for schema authoring where TypeScript-schema coupling is useful, JSON Schema artifacts for persisted format contracts, and AJV for runtime validation.
38
- - YAML: `yaml`, matching pi's dependency choice.
39
- - Merge behavior: `defu`, so Outfitter can use controlled deep defaults while documenting key-specific array behavior.
40
- - Process launch: `cross-spawn`, matching pi's dependency choice and avoiding platform-specific spawn edge cases.
41
- - Filesystem discovery and URI parsing: `glob` and `hosted-git-info`, aligned with pi where compatible with Node `>=22.19.0`.
42
-
43
- ### Initial npm Dependencies
44
-
45
- Runtime dependencies in the first `package.json`:
46
-
47
- - `commander`: CLI parsing, default command behavior, aliases, and pass-through argument support.
48
- - `yaml`: YAML parsing and serialization for user-editable config.
49
- - `ajv`: JSON Schema validation at file-read boundaries.
50
- - `typebox`: Type-friendly schema definitions and schema-derived types where useful.
51
- - `defu`: controlled deep merging for settings and profiles.
52
- - `liquidjs`: safe Outfitter-time composite profile templating with custom delimiters that avoid common agent template syntaxes.
53
- - `cross-spawn`: portable child process launch for agent CLIs.
54
- - `glob`: profile/resource discovery.
55
- - `hosted-git-info`: parsing hosted git URIs for sync/cache handling; pinned to the latest line compatible with Node `>=22.19.0`.
56
- - `chalk`: readable terminal diagnostics.
57
-
58
- Development dependencies:
59
-
60
- - `typescript`
61
- - `vitest`
62
- - `@vitest/coverage-v8`
63
- - `eslint`
64
- - `@eslint/js`
65
- - `typescript-eslint`
66
- - `@types/node`
67
- - `@types/cross-spawn`
68
- - `shx`
69
-
70
- The project includes `tsconfig.json` for strict typechecking across source, tests, and config, plus `tsconfig.build.json` for production emission from `src/` to `dist/`.
71
-
72
- ## Repository File Structure
73
-
74
- The repository layout and source/test directory boundaries live in `doc/file_structure.md`.
75
-
76
- ## Settings Resolution
77
-
78
- Outfitter uses a `.outfitter` folder convention at multiple scopes:
79
-
80
- ```text
81
- ~/.outfitter/
82
- settings.yml
83
- profiles
84
- cache/
85
- profiles/
86
- utilities/
87
-
88
- <project>/.outfitter/
89
- settings.yml
90
- profiles/
91
-
92
- <project>/.outfitter/local/
93
- settings.yml
94
- profiles/
95
- ```
96
-
97
- All discovered `settings.yml` files are collectively referred to as Outfitter settings.
98
- The internal `Settings` object is the single conceptual result of reading all settings sources and applying precedence.
99
-
100
- Note they are all the same in conceptual structure, with the exceptions that the <project>/.outfitter/ contains the local one inside it, and the ~/.outfitter includes the cache folder.
101
-
102
- ### Settings Precedence
103
-
104
- Highest to lowest:
105
-
106
- 1. Project-local: `<project>/.outfitter/local/settings.yml`
107
- 2. Project: `<project>/.outfitter/settings.yml`
108
- 3. User: `~/.outfitter/settings.yml`
109
- 4. Cached remote settings referenced by local settings
110
- 5. Built-in defaults
111
-
112
- Additional sources can be added behind the same `SettingsLoader` abstraction.
113
-
114
- ### Required User Default Profile
115
-
116
- `~/.outfitter/settings.yml` MUST declare a default profile after setup completes.
117
- This guarantees `outfitter run` can resolve a profile even when no `--profile` is provided.
118
-
119
- Example:
120
-
121
- ```yaml
122
- # ~/.outfitter/settings.yml
123
- default_profile: user_default
124
-
125
- profile_sources:
126
- - path: ./profiles
127
- ```
128
-
129
- A minimal user profile tree for that settings file looks like this:
130
-
131
- ```text
132
- ~/.outfitter/
133
- settings.yml
134
- profiles/
135
- user_default/
136
- profile.yml
137
- prompts/
138
- system.md
139
- ```
140
-
141
- ## Settings Schema
142
-
143
- `settings.yml` supports:
144
-
145
- ```yaml
146
- default_profile: engineering
147
- default_agent: pi
148
- cache_directory: ./cache
149
-
150
- profile_sources:
151
- - path: ./profiles
152
- only:
153
- - engineering
154
- - support
155
-
156
- - uri: git+https://github.com/example/company-outfitter-profiles.git
157
- ref: main
158
- path: profiles/team
159
- except:
160
- - experimental
161
-
162
- - github: example/outfitter-config
163
- ref: main
164
- path: profiles
165
-
166
- remote_settings:
167
- - github: example/outfitter-config
168
- ref: main
169
- path: settings.yml
170
-
171
- profiles:
172
- engineering:
173
- # Inline profile fragments are allowed if schema support is retained.
174
- ```
175
-
176
- Rules:
177
-
178
- - Every settings file MUST validate against `settings.schema.json`.
179
- - `default_agent`, when present, selects the run adapter (`pi` or `claude`) used when `outfitter run --agent` is omitted.
180
- - `profile_sources` entries MUST specify a local `path`, a remote `uri`, or a `github` shorthand.
181
- - `only` and `except` are optional filters; without either, all profiles from the source are loaded.
182
- - Local-only relative `path` values are resolved relative to the settings file containing them.
183
- - `cache_directory` optionally selects the Outfitter cache root; relative values are resolved relative to the settings file containing them.
184
- - Remote `uri` and `github` profile sources can specify `ref` and repository-subdirectory `path` values.
185
- - `remote_settings` entries point at settings-style YAML files inside synced remote repositories.
186
- - `uri`, `github`, and `remote_settings` sources are fetched/cached by `outfitter sync`.
187
- - `custom_settings` may contain arbitrary YAML-compatible nested data.
188
- Outfitter deep-merges custom settings objects using normal settings precedence; arrays and scalar values are replaced by the higher-precedence settings layer.
189
-
190
- ### Settings File Examples
191
-
192
- A user settings file can select a default profile and expose user-managed profiles:
193
-
194
- ```yaml
195
- # ~/.outfitter/settings.yml
196
- default_profile: personal-engineering
197
- cache_directory: ./cache
198
-
199
- profile_sources:
200
- - path: ./profiles
201
-
202
- custom_settings:
203
- build_commands:
204
- lint: npm run lint
205
- test: npm test
206
- ```
207
-
208
- A project settings file can add checked-in project profiles and remote organizational profiles:
209
-
210
- ```yaml
211
- # <project>/.outfitter/settings.yml
212
- default_profile: project-engineering
213
-
214
- profile_sources:
215
- - path: ./profiles
216
- - github: example/company-outfitter-config
217
- ref: main
218
- path: profiles/shared
219
- only:
220
- - base-typescript
221
- - secure-review
222
-
223
- remote_settings:
224
- - github: example/company-outfitter-config
225
- ref: main
226
- path: settings.yml
227
- ```
228
-
229
- A project-local settings file can override the default profile without changing checked-in files:
230
-
231
- ```yaml
232
- # <project>/.outfitter/local/settings.yml
233
- default_profile: local-sandbox
234
-
235
- profile_sources:
236
- - path: ./profiles
237
- ```
238
-
239
- ## Composite profile Template Rendering
240
-
241
- Outfitter renders Outfitter-time templates in generated composite profile files after profile and settings resolution and before writing the composite profile directory to disk.
242
- Source settings files are never rewritten.
243
-
244
- Outfitter uses LiquidJS with custom delimiters rather than common `{{ ... }}` / `{% ... %}` delimiters so templates do not collide with common agent prompt, skill, command, Handlebars, Mustache, Jinja, or Claude Code syntaxes.
245
-
246
- Canonical delimiters:
247
-
248
- ```text
249
- [[= expression ]] output expression
250
- [[% tag %]] Liquid control tag, such as if, for, endif, or endfor
251
- ```
252
-
253
- Outfitter only treats `[[=` and `[[%` as template openers, so plain shell or Bash expressions such as `[[ -f package.json ]]` pass through unchanged.
254
-
255
- Template context:
256
-
257
- ```yaml
258
- outfitter:
259
- custom_settings: # resolved settings.custom_settings
260
- settings: # resolved settings using YAML-style key names
261
- profile: # resolved profile object
262
- agent: pi
263
- project:
264
- root: /absolute/project/root
265
- ```
266
-
267
- Example:
268
-
269
- ```yaml
270
- # settings.yml
271
- custom_settings:
272
- build_commands:
273
- lint: npm run lint
274
- test: npm test
275
- ```
276
-
277
- ```yaml
278
- # any generated composite profile settings file containing Outfitter template delimiters
279
- hooks:
280
- lint:
281
- command: "[[= outfitter.custom_settings.build_commands.lint ]]"
282
-
283
- [[% if outfitter.custom_settings.build_commands.test %]]
284
- test:
285
- command: "[[= outfitter.custom_settings.build_commands.test ]]"
286
- [[% endif %]]
287
- ```
288
-
289
- Liquid loops and filters are available with the same custom tag and output delimiters:
290
-
291
- ```yaml
292
- commands:
293
- [[% for command in outfitter.custom_settings.commands %]]
294
- - "[[= command ]]"
295
- [[% endfor %]]
296
- ```
297
-
298
- Undefined output variables and unknown filters are template errors.
299
- Undefined variables in `if`, `elsif`, and `unless` conditions are allowed and evaluate as falsy so templates can test optional custom settings.
300
- Template errors identify the composite profile file being rendered and stop composite profile assembly.
301
-
302
- ## Profile Sources and Sync
303
-
304
- A profile source can be local, URI-based, or GitHub shorthand-based.
305
-
306
- ### Local Source
307
-
308
- ```yaml
309
- profile_sources:
310
- - path: ./profiles
311
- ```
312
-
313
- The path points to a folder containing profile folders, not a specific profile folder.
314
-
315
- ### URI or GitHub Source
316
-
317
- ```yaml
318
- profile_sources:
319
- - uri: git+ssh://git@github.com/example/company-profiles.git
320
- ref: main
321
- path: profiles/team
322
-
323
- - github: example/company-profiles
324
- ref: main
325
- path: profiles/team
326
- ```
327
-
328
- The `github: owner/repo` shorthand normalizes to `git+https://github.com/owner/repo.git` internally.
329
- Remote sources without `ref` or repository subpaths retain the original profile cache location for compatibility:
330
-
331
- ```text
332
- ~/.outfitter/cache/profiles/<encoded-uri>/
333
- ```
334
-
335
- Remote sources that specify `ref`, repository-subdirectory `path`, or `github` are fetched into the shared repository cache:
336
-
337
- ```text
338
- ~/.outfitter/cache/repos/<encoded-uri-and-ref>/
339
- ```
340
-
341
- Profile loading then reads from the requested subdirectory inside the cached repository.
342
-
343
- ### Remote Settings Source
344
-
345
- ```yaml
346
- remote_settings:
347
- - github: example/outfitter-config
348
- ref: main
349
- path: settings.yml
350
- ```
351
-
352
- `outfitter sync` fetches remote settings repositories into the shared repository cache, validates that the requested settings file exists, then loads cached remote settings as lower-precedence settings sources during later settings resolution.
353
-
354
- ## Profile Layout
355
-
356
- A profile is a folder with a required `profile.yml` file:
357
-
358
- ```text
359
- profiles/
360
- engineering/
361
- profile.yml
362
- prompts/
363
- skills/
364
- extensions/
365
- deepwork/
366
- jobs/
367
- cli_specific/
368
- pi/
369
- claude/
370
- ```
371
-
372
- Example `profile.yml`:
373
-
374
- ```yaml
375
- id: engineering
376
- label: Engineering
377
- description: General software engineering profile for coding, tests, reviews, and repo navigation.
378
- template: false
379
- inherits:
380
- - base-typescript
381
-
382
- controls:
383
- model: anthropic/claude-sonnet-4
384
- system_prompt: ./prompts/system.md
385
- append_system_prompt: ./prompts/company-policy.md
386
- skills:
387
- - ./skills/debugging
388
- extensions:
389
- - ./extensions/company-bootstrap
390
- environment:
391
- TEAM_MODE: engineering
392
- # Omit provider API keys here to inherit them from the parent environment.
393
- pi:
394
- args:
395
- - --thinking
396
- - medium
397
- ```
398
-
399
- Rules:
400
-
401
- - Every `profile.yml` MUST validate against `profile.schema.json`.
402
- - `inherits` is an ordered array of profile names.
403
- - `template: true` marks a profile as inheritance-only: it may contribute controls to runnable profiles through `inherits`, but `outfitter run --profile <id>` and `default_profile: <id>` launches reject it directly.
404
- - `skills/` contains profile-bundled Agent Skills. Outfitter exposes valid skills from contributing profile folders when launching Pi.
405
- - `deepwork/jobs/` contains profile-bundled DeepWork jobs. Outfitter appends contributing profile job folders to `DEEPWORK_ADDITIONAL_JOBS_FOLDERS` when launching Pi so DeepWork can discover profile-owned workflows.
406
- Profile-bundled jobs are isolated by default: inherited `DEEPWORK_ADDITIONAL_JOBS_FOLDERS` entries are included only when the profile sets `controls.pi.allow_external_deepwork_jobs: true`.
407
- - `cli_specific/<cli-name>/` contains files copied or translated directly into the generated composite profile for that CLI, plus resources that intentionally apply only to one adapter.
408
- - Pi profiles may provide `cli_specific/pi/.mcp.json`; Outfitter merges contributing profile fragments into the composite profile with unique array entries by identity and last writer wins for duplicate identities.
409
- - Pi-specific skills and DeepWork jobs may also live under `cli_specific/pi/skills/` and `cli_specific/pi/deepwork/jobs/` when they should not be exposed to other adapters.
410
- - `append_system_prompt` accepts a string or an ordered string array. When multiple resolved profile layers provide it, Outfitter composes the values into repeated agent append-prompt inputs in profile precedence order so shared prompt profiles do not need to use raw CLI `args`.
411
- - CLI-specific configuration wins over generic controls when both apply to the same generated artifact.
412
-
413
- ### Profile Directory Examples
414
-
415
- A small user-level profile set can keep a base profile and a specialized profile side by side:
416
-
417
- ```text
418
- ~/.outfitter/profiles/
419
- base-typescript/
420
- profile.yml
421
- prompts/
422
- system.md
423
- personal-engineering/
424
- profile.yml
425
- prompts/
426
- system.md
427
- skills/
428
- debugging/
429
- SKILL.md
430
- ```
431
-
432
- ```yaml
433
- # ~/.outfitter/profiles/base-typescript/profile.yml
434
- id: base-typescript
435
- label: Base TypeScript
436
-
437
- controls:
438
- provider: anthropic
439
- model: anthropic/claude-sonnet-4
440
- thinking: medium
441
- system_prompt: ./prompts/system.md
442
- environment:
443
- NODE_ENV: development
444
- ```
445
-
446
- ```yaml
447
- # ~/.outfitter/profiles/shared-prose/profile.yml
448
- id: shared-prose
449
- label: Shared Prose
450
- template: true
451
-
452
- controls:
453
- append_system_prompt: ./prompts/prose.md
454
- ```
455
-
456
- ```yaml
457
- # ~/.outfitter/profiles/personal-engineering/profile.yml
458
- id: personal-engineering
459
- label: Personal Engineering
460
- inherits:
461
- - base-typescript
462
- - shared-prose
463
-
464
- controls:
465
- append_system_prompt: ./prompts/system.md
466
- skills:
467
- - ./skills/debugging
468
- pi:
469
- args:
470
- - --no-themes
471
- ```
472
-
473
- If no resolved profile folder provides `cli_specific/<adapter>/<state-path>` for an adapter-declared symlink path, Outfitter falls back directly to the native CLI state location for that path, such as `~/.pi/agent/settings.json` or `~/.claude/settings.json`.
474
- That native fallback is not a hidden base profile: it does not participate in `inherits`, cannot contribute profile controls, and only supplies durable symlink targets for declared CLI state.
475
-
476
- A checked-in project profile set can add project-specific prompts and pi resources:
477
-
478
- ```text
479
- <project>/.outfitter/profiles/
480
- project-engineering/
481
- profile.yml
482
- prompts/
483
- system.md
484
- review.md
485
- extensions/
486
- project-bootstrap/
487
- package.json
488
- src/
489
- index.ts
490
- cli_specific/
491
- pi/
492
- settings.json
493
- ```
494
-
495
- ```yaml
496
- # <project>/.outfitter/profiles/project-engineering/profile.yml
497
- id: project-engineering
498
- label: Project Engineering
499
- inherits:
500
- - base-typescript
501
-
502
- controls:
503
- system_prompt: ./prompts/system.md
504
- append_system_prompt: ./prompts/review.md
505
- extensions:
506
- - ./extensions/project-bootstrap
507
- session_directory: ./.outfitter/sessions/project-engineering
508
- pi:
509
- prompt_template: code-review
510
- args:
511
- - --model
512
- - anthropic/claude-sonnet-4
513
- ```
514
-
515
- A local-only sandbox profile can use the highest-precedence project-local layer:
516
-
517
- ```text
518
- <project>/.outfitter/local/
519
- settings.yml
520
- profiles/
521
- local-sandbox/
522
- profile.yml
523
- prompts/
524
- sandbox.md
525
- ```
526
-
527
- ```yaml
528
- # <project>/.outfitter/local/profiles/local-sandbox/profile.yml
529
- id: local-sandbox
530
- label: Local Sandbox
531
-
532
- controls:
533
- system_prompt: ./prompts/sandbox.md
534
- environment:
535
- OUTFITTER_EXPERIMENTAL_MODE: '1'
536
- pi:
537
- thinking: high
538
- ```
539
-
540
- ## Profile Resolution and Inheritance
541
-
542
- When `outfitter run --profile X` is invoked, Outfitter builds an ordered profile stack.
543
-
544
- Highest to lowest precedence:
545
-
546
- 1. Project-local profile `X`
547
- 2. Project profile `X`
548
- 3. User profile `X`
549
- 4. URI/cache profile `X` according to source order
550
- 5. Profiles explicitly inherited by `X`, recursively, in declared order
551
- 6. The user default profile as an implicit bottom profile, recursively including its inherited profiles
552
- 7. Built-in defaults
553
-
554
- Notes:
555
-
556
- - Cycles in `inherits` MUST be detected and reported as validation errors.
557
- - Profile merging should use `defu` or a similar controlled deep-merge utility.
558
- - YAML merge behavior should be explicitly documented per key; arrays should not be blindly merged where order or duplication matters.
559
-
560
- ## Composite profile Model
561
-
562
- A **composite profile** is the dynamically assembled runtime configuration directory for a specific profile and agent CLI.
563
-
564
- Example term usage: “the composite profile for `data-analyst` on `claude`”.
565
-
566
- Composite profiles are generated under the system temp directory so they can be reclaimed trivially, while adapter-declared state paths can be symlinked to durable profile, native CLI, or Outfitter cache locations:
567
-
568
- ```text
569
- $TMPDIR/outfitter-<profile-id>-<agent-id>-<random>/
570
- ```
571
-
572
- The pi adapter uses this state model for native pi state and for pi-managed utilities: most declared pi state paths, including `tmp/`, symlink to `~/.pi/agent/<path>` by default, while composite profile `utilities/` and `bin/` both symlink to `<cache_directory>/utilities` by default, so temporary composite profile cleanup does not force pi to redownload helper binaries such as `fd` and `rg`.
573
-
574
- When profile-controlled Pi extensions would duplicate native Pi `settings.json` package entries, Outfitter generates a reconciled runtime `settings.json` inside the composite profile for that launch.
575
- The generated file preserves unrelated settings and package entries, removes duplicate native package entries by normalized resource identity, and keeps the declared `settings.json` state path non-durable with `discard` write handling so runtime edits are not reported as unknown state.
576
-
577
- During `outfitter run`, the Outfitter process remains alive while the child agent CLI runs.
578
- It owns the composite profile lifecycle.
579
-
580
- ### Functional State Updating Model
581
-
582
- Outfitter treats agent state updates as an explicit product behavior rather than an accidental side effect of temporary composite profile files.
583
- Before launch, the selected adapter names the paths the agent CLI is expected to mutate, such as authentication files, settings files, plugin folders, caches, and sessions.
584
- The resolved profile then chooses a functional persistence strategy for each path:
585
-
586
- - `symlink`: writes are durable because the composite profile path points at a profile-managed or native CLI state path.
587
- - `discard`: writes are allowed for the run but thrown away with the temporary composite profile.
588
- - `warn`: writes are allowed and discarded, then reported after exit.
589
- - `error`: writes are allowed during the run but make the Outfitter command fail after exit.
590
- - `prompt`: reserved for future interactive handling; currently treated as a non-persistent diagnostic.
591
-
592
- Unknown writes are handled separately from declared state paths.
593
- They are never silently persisted because Outfitter has no declared durable destination for them.
594
- The adapter's `unknown` policy decides whether to discard, warn, error, or eventually prompt.
595
-
596
- The practical user model is:
597
-
598
- 1. Put durable, editable CLI state under `cli_specific/<agent>/` in a profile, or let Outfitter fall back to the native CLI state location.
599
- 2. Use `state_persistence` in `profile.yml` only for paths that should deviate from the adapter defaults.
600
- 3. Use `warn` or `error` for strict profiles and CI when unexpected state mutation should be visible.
601
- 4. Use `discard` for caches, sessions, or experimental state that should not outlive the run.
602
-
603
- See `doc/state_writeback_strategy.md` for the complete functional contract and the current pi path policy.
604
-
605
- ### Composite profile Assembly
606
-
607
- `CompositeProfileAssembler` resolves:
608
-
609
- 1. the requested profile stack;
610
- 2. generic controls;
611
- 3. CLI-specific overrides;
612
- 4. generated files;
613
- 5. child process env and argv.
614
-
615
- Each logical file in the composite profile has an object instance, represented by `CompositeProfileFile`, that knows:
616
-
617
- - source inputs;
618
- - generated output path;
619
- - merge/transform strategy;
620
- - validation rules;
621
- - unsupported-control warnings;
622
- - live synchronization behavior.
623
-
624
- ### Live Synchronization
625
-
626
- While the child agent process runs:
627
-
628
- - `fs.watch` runs on input files/folders used by each logical `CompositeProfileFile`;
629
- - changed inputs are revalidated and regenerated into the composite profile where safe;
630
- - unsupported or unsafe live updates produce warnings;
631
- - fatal composite profile errors stop the run only when `--strict` is enabled or when the child CLI cannot continue safely.
632
-
633
- ## Agent Adapter Boundary
634
-
635
- Each supported CLI has an `AgentAdapter` implementation.
636
-
637
- ```ts
638
- interface AgentAdapter {
639
- readonly id: string;
640
- readonly supportedControls: readonly string[];
641
- readonly statePaths?: Readonly<Record<string, StatePathDeclaration>>;
642
- createCompositeProfile(
643
- profile: Profile,
644
- input: {
645
- readonly rootDirectory: string;
646
- readonly profilePaths: readonly string[];
647
- readonly profileFolders?: readonly string[];
648
- readonly homeDirectory?: string;
649
- readonly cacheDirectory?: string;
650
- readonly settings?: Settings;
651
- readonly projectDirectory?: string;
652
- },
653
- ): AgentCompositeProfilePlan;
654
- createLaunchPlan(
655
- compositeProfile: CompositeProfile,
656
- profile?: Profile,
657
- passThroughArgs?: readonly string[],
658
- ): AgentLaunchPlan;
659
- getUnsupportedControls(profile: Profile): readonly string[];
660
- }
661
- ```
662
-
663
- The adapter owns CLI-specific details such as env vars, flags, state path declarations, warnings, and unsupported controls.
664
-
665
- ### Supported Adapters: Pi and Claude Code
666
-
667
- Pi is the default adapter for backward compatibility.
668
- Outfitter should prefer native pi mechanisms:
669
-
670
- - `PI_CODING_AGENT_DIR` for profile-scoped global state;
671
- - `PI_CODING_AGENT_SESSION_DIR` or `--session-dir` for sessions;
672
- - `--extension` / `-e` for explicit extensions;
673
- - `--skill` for explicit skills;
674
- - `--prompt-template` for prompt templates;
675
- - `--system-prompt` and `--append-system-prompt` for prompts;
676
- - model/provider/thinking flags where supported;
677
- - generated Pi settings reconciliation in the composite profile where flags/env are not the right mechanism.
678
-
679
- If generic Outfitter controls conflict with pi naming or behavior, prefer pi’s terminology and conventions.
680
- Because Outfitter chooses `PI_CODING_AGENT_DIR` and other startup-sensitive configuration before launching pi, a requested pi control that would require changing startup discovery after pi has already begun cannot be applied by an extension or late runtime hook.
681
- The pi adapter reports such unsupported or startup-order-impossible controls through normal adapter warnings, and `--strict` makes those warnings fatal.
682
-
683
- Claude Code is also supported through the `claude` adapter.
684
- Outfitter launches `claude` with `CLAUDE_CONFIG_DIR` pointing at the composite profile root, maps supported controls to native flags (`--model`, `--effort`, `--system-prompt`, `--append-system-prompt`, and repeated `--plugin-dir`), and preserves Claude Code state paths such as `settings.json`, `agents/`, `skills/`, `commands/`, `plugins/`, `projects/`, and `debug/` through adapter-declared state persistence.
685
- Claude-specific profile overrides live under `controls.claude` and win over generic controls for Claude runs.
686
-
687
- ## CLI Commands
688
-
689
- ### `outfitter run`
690
-
691
- `run` is the default command when no command is specified.
692
-
693
- Examples:
694
-
695
- ```bash
696
- outfitter
697
- outfitter run
698
- outfitter run --profile engineering
699
- outfitter run -p support -- --model anthropic/claude-sonnet-4
700
- outfitter run --agent claude -p support -- --permission-mode plan
701
- ```
702
-
703
- Requirements:
704
-
705
- - `-p` / `--profile` selects a profile.
706
- - `--agent <pi|claude>` selects the agent adapter; if omitted, `default_agent` from settings is used, then `pi`.
707
- - Without a selected profile, the unified settings default profile is used.
708
- - Unknown args are passed through to the inner agent CLI unaltered.
709
- - `--strict` makes unsupported profile controls or composite profile assembly warnings fatal.
710
- - The child agent CLI runs with the generated composite profile, env, and argv.
711
-
712
- ### `outfitter setup`
713
-
714
- Responsibilities:
715
-
716
- - create `~/.outfitter/settings.yml` when missing;
717
- - accept an optional setup source URI, for example `outfitter setup https://github.com/example/outfitter-config`, and clone/update it under `~/.outfitter/cache/repos/<encoded-uri-and-ref>/`;
718
-
719
- - when a setup source is provided, use its root `settings.yml` or `.outfitter/settings.yml` and `profiles/` or `.outfitter/profiles/` as the initial non-overwriting setup starting point for the selected import target;
720
- - during interactive setup-source onboarding, show the Outfitter welcome first, explain the source being imported, ask whether to install into user home or the current project, then ask exactly one source-profile/default prompt;
721
- - require an interactive TTY on both stdin and stdout before running setup prompts;
722
- - create a default profile when missing;
723
- - validate all discovered settings files and any starter settings file;
724
- - run `outfitter sync` behavior for URI profile sources before profile selection;
725
-
726
- - outside that initial welcome handoff and outside setup-source import onboarding, show a setup wizard with synced profile choices, preserve display labels where available, validate the selected profile ID, and write the selected default profile to user settings;
727
- - create any missing fallback default profile file for the final selected default profile;
728
- - report actionable next steps.
729
-
730
- ### `outfitter welcome`
731
-
732
- Responsibilities:
733
-
734
- - require an interactive TTY on both stdin and stdout before running welcome prompts;
735
- - show welcome text that explains Outfitter and Pi before asking onboarding questions;
736
- - ask whether the user wants to answer setup questions now;
737
- - ask the user to choose an initial built-in standard role, currently including `engineer` and `data_analyst`;
738
- - recommend a named Pi productivity loadout containing `git:github.com/ai-outfitter/ulta-tasklist`, `git:github.com/ai-outfitter/deepwork`, `npm:pi-subagents`, and `npm:pi-mcp-adapter`;
739
- - allow the user to accept the loadout, choose individual loadout items, or skip loadout installation;
740
- - create the selected local role profile on the fly, appending only the selected loadout resources and leaving extensions/skills empty when the user selects none;
741
- - return typed onboarding choices so later work can persist richer profile/loadout metadata behind a schema-validated YAML format if needed.
742
-
743
- Before launching Pi after welcome onboarding, `outfitter run` checks whether native Pi appears to have login state in `auth.json` or `models.json`.
744
- If no login state is detected, Outfitter starts Pi with `/login` automatically; outside the welcome flow it prints an informational `/login` notice instead.
745
- Credential collection stays inside Pi so provider API keys are not collected or persisted by Outfitter.
746
-
747
- ### `outfitter sync`
748
-
749
- Responsibilities:
750
-
751
- - read settings;
752
- - validate profile sources;
753
- - fetch/update URI-based, GitHub shorthand, and remote settings sources;
754
- - store plain URI profile sources without `ref` or repository subpaths under `~/.outfitter/cache/profiles/<encoded-uri>/` for compatibility;
755
- - store GitHub shorthand sources and sources with `ref` or repository subpaths under `~/.outfitter/cache/repos/<encoded-uri-and-ref>/`;
756
- - validate fetched remote settings files and profiles;
757
- - report whether each source was updated, unchanged, skipped, or failed;
758
- - redact credentials embedded in source URIs from user-facing output.
759
-
760
- ### `outfitter profile list`
761
-
762
- Lists profile IDs from configured local and cached remote profile sources.
763
-
764
- Responsibilities:
765
-
766
- - read and validate settings;
767
- - load configured local and cached remote profile sources;
768
- - hide template profiles by default because they are inheritance-only;
769
- - include template profiles when `--all` is requested and mark them as templates in output;
770
- - report unique profile IDs deterministically;
771
- - use the highest-precedence loaded definition when duplicate profile IDs exist.
772
-
773
- ### `outfitter profile create`
774
-
775
- Creates a placeholder profile folder at a requested scope.
776
-
777
- Example shape:
778
-
779
- ```bash
780
- outfitter profile create engineering --scope user
781
- outfitter profile create support --scope project
782
- outfitter profile create sandbox --scope project-local
783
- ```
784
-
785
- Responsibilities:
786
-
787
- - require an explicit destination scope or path;
788
- - require a profile name;
789
- - create `profile.yml` and conventional subfolders;
790
- - optionally add the containing folder to `profile_sources` if needed;
791
- - validate the generated profile.
792
-
793
- ## Command Object Pattern
794
-
795
- Each non-trivial command should be implemented as a command object rather than embedding logic inside CLI parser callbacks.
796
-
797
- ```ts
798
- class RunCommand {
799
- constructor(
800
- private readonly settingsLoader: SettingsLoader,
801
- private readonly profileLoader: ProfileLoader,
802
- private readonly composite profileAssembler: Composite profileAssembler,
803
- private readonly processRunner: ProcessRunner,
804
- ) {}
805
-
806
- async execute(input: RunCommandInput): Promise<RunCommandResult> {
807
- // parse-resolved inputs only; no direct process.argv access here
808
- }
809
- }
810
- ```
811
-
812
- Benefits:
813
-
814
- - easier unit testing;
815
- - lower CLI parser coupling;
816
- - explicit dependencies;
817
- - natural enforcement of complexity limits.
818
-
819
- ## Validation Strategy
820
-
821
- Validation happens at every file boundary:
822
-
823
- - settings read: `settings.schema.json`;
824
- - profile read: `profile.schema.json`;
825
- - generated composite profile metadata: composite profile schema, if persisted;
826
- - command inputs: typed parser output plus runtime validation;
827
- - URI cache metadata: schema-backed YAML.
828
-
829
- YAML parsing should preserve helpful source locations where practical so diagnostics can point to the file and key that failed.
830
-
831
- ## Test Strategy
832
-
833
- The test suite should be created before feature implementation becomes large.
834
-
835
- Requirements:
836
-
837
- - global coverage threshold: 100% for statements, branches, functions, and lines;
838
- - deterministic tests for settings precedence;
839
- - deterministic tests for profile inheritance and cycle detection;
840
- - deterministic tests for URI cache path encoding;
841
- - deterministic tests for generated pi launch env/argv;
842
- - deterministic tests for unsupported controls and `--strict`;
843
- - scenario fixtures for common combinations instead of one-off bespoke setup.
844
-
845
- Scenario fixture directory conventions are documented in `doc/file_structure.md`.
846
- Each scenario should include realistic `.outfitter` folders and expected resolution output.
847
-
848
- ## Settled Initial Decisions
849
-
850
- 1. Outfitter uses npm and commits `package-lock.json`.
851
- 2. Outfitter uses Commander for CLI parsing.
852
- 3. Outfitter uses Vitest with V8 coverage for tests.
853
- 4. Outfitter profile IDs are filesystem-safe slugs; optional display names can carry spaces or punctuation.
854
- 5. Profile-management commands use a resource namespace: `profile list` and `profile create`.
855
- 6. URI profile source lockfiles are deferred beyond v1; v1 sync records cache metadata but does not require lockfile-driven reproducibility.
856
- 7. Claude Code is supported as an additional adapter, while pi remains the default adapter.