@ai-outfitter/outfitter 1.5.0 → 1.7.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.
Files changed (83) hide show
  1. package/README.md +11 -2
  2. package/code/pi-extension/src/outfitter-extension.js +9 -1
  3. package/dist/agents/AgentLaunch.d.ts +2 -2
  4. package/dist/agents/AgentLaunch.js +4 -3
  5. package/dist/agents/AgentLaunch.js.map +1 -1
  6. package/dist/agents/ClaudeStatePersistence.d.ts +18 -0
  7. package/dist/agents/ClaudeStatePersistence.js +225 -0
  8. package/dist/agents/ClaudeStatePersistence.js.map +1 -0
  9. package/dist/cli/OutfitterCli.js +1 -1
  10. package/dist/cli/OutfitterCli.js.map +1 -1
  11. package/dist/cli/commands/DumpCommand.d.ts +1 -0
  12. package/dist/cli/commands/DumpCommand.js +11 -1
  13. package/dist/cli/commands/DumpCommand.js.map +1 -1
  14. package/dist/cli/commands/ListCommand.d.ts +2 -0
  15. package/dist/cli/commands/ListCommand.js +15 -4
  16. package/dist/cli/commands/ListCommand.js.map +1 -1
  17. package/dist/cli/commands/RunAgentCommand.js +85 -17
  18. package/dist/cli/commands/RunAgentCommand.js.map +1 -1
  19. package/dist/cli/commands/SyncCommand.d.ts +8 -3
  20. package/dist/cli/commands/SyncCommand.js +126 -21
  21. package/dist/cli/commands/SyncCommand.js.map +1 -1
  22. package/dist/cli/commands/ValidateCommand.js +1 -1
  23. package/dist/cli/commands/ValidateCommand.js.map +1 -1
  24. package/dist/projection/CodexMcp.d.ts +11 -0
  25. package/dist/projection/CodexMcp.js +143 -0
  26. package/dist/projection/CodexMcp.js.map +1 -0
  27. package/dist/projection/Materialize.d.ts +3 -1
  28. package/dist/projection/Materialize.js +7 -3
  29. package/dist/projection/Materialize.js.map +1 -1
  30. package/dist/projection/ProjectHarness.js +66 -29
  31. package/dist/projection/ProjectHarness.js.map +1 -1
  32. package/dist/projection/Projection.d.ts +2 -0
  33. package/dist/resolver/AmbiguityWarnings.d.ts +7 -0
  34. package/dist/resolver/AmbiguityWarnings.js +86 -0
  35. package/dist/resolver/AmbiguityWarnings.js.map +1 -0
  36. package/dist/resolver/Layer.d.ts +8 -2
  37. package/dist/resolver/Layer.js +48 -8
  38. package/dist/resolver/Layer.js.map +1 -1
  39. package/dist/resolver/ResolverContext.d.ts +3 -1
  40. package/dist/resolver/ResolverContext.js +11 -2
  41. package/dist/resolver/ResolverContext.js.map +1 -1
  42. package/dist/resolver/ResolverValidation.d.ts +12 -1
  43. package/dist/resolver/ResolverValidation.js +30 -20
  44. package/dist/resolver/ResolverValidation.js.map +1 -1
  45. package/dist/schemas/settings.schema.json +1 -1
  46. package/dist/schemas/system-extension-hook.schema.json +38 -0
  47. package/dist/settings/Settings.d.ts +1 -1
  48. package/dist/settings/Settings.js +1 -1
  49. package/dist/settings/Settings.js.map +1 -1
  50. package/dist/settings/SettingsLoader.js.map +1 -1
  51. package/dist/setup/DefaultCatalog.d.ts +18 -0
  52. package/dist/setup/DefaultCatalog.js +74 -13
  53. package/dist/setup/DefaultCatalog.js.map +1 -1
  54. package/dist/sources/SourceCache.d.ts +20 -0
  55. package/dist/sources/SourceCache.js +31 -1
  56. package/dist/sources/SourceCache.js.map +1 -1
  57. package/dist/sources/TransitiveSources.d.ts +45 -0
  58. package/dist/sources/TransitiveSources.js +179 -0
  59. package/dist/sources/TransitiveSources.js.map +1 -0
  60. package/dist/system/SystemExtensionHook.d.ts +34 -0
  61. package/dist/system/SystemExtensionHook.js +190 -0
  62. package/dist/system/SystemExtensionHook.js.map +1 -0
  63. package/dist/validation/SchemaValidator.d.ts +1 -1
  64. package/dist/validation/SchemaValidator.js +2 -0
  65. package/dist/validation/SchemaValidator.js.map +1 -1
  66. package/docs/architecture/state_writeback_strategy.md +15 -3
  67. package/docs/documentation/README.md +14 -1
  68. package/docs/documentation/catalogs.md +61 -2
  69. package/docs/documentation/cli.md +2 -1
  70. package/docs/documentation/concepts.md +2 -2
  71. package/docs/documentation/hooks.md +33 -0
  72. package/docs/documentation/in-cluster.md +2 -0
  73. package/docs/documentation/migration.md +1 -1
  74. package/docs/documentation/personas.md +1 -1
  75. package/docs/documentation/settings.md +1 -1
  76. package/docs/documentation/state.md +27 -0
  77. package/docs/documentation/support-matrix.md +34 -23
  78. package/docs/documentation/usecases/org-onboarding-sdlc-report.md +143 -0
  79. package/docs/documentation/usecases/persona-reviews.md +1 -1
  80. package/docs/philosophy.md +20 -0
  81. package/package.json +1 -1
  82. package/src/schemas/settings.schema.json +1 -1
  83. package/src/schemas/system-extension-hook.schema.json +38 -0
@@ -1,7 +1,13 @@
1
1
  // Encodes remote `.agents` source cache paths and normalizes remote source references.
2
- import { isAbsolute, join, relative, resolve } from 'node:path';
2
+ import { isAbsolute, join, posix, relative, resolve } from 'node:path';
3
3
  /** Distinguishes remote (`uri`/`github`) sources from local `path` sources. */
4
4
  export const isRemoteSource = (source) => source.uri !== undefined || source.github !== undefined;
5
+ const fullCommitPattern = /^[a-f0-9]{40}$/u;
6
+ const versionTagPattern = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
7
+ /** True for a ref that names one immutable revision: a full commit SHA or a version tag. */
8
+ export const isImmutableRef = (ref) => fullCommitPattern.test(ref) || versionTagPattern.test(ref);
9
+ /** True for an immutable ref that must be resolved to a commit via `rev-parse` (a version tag). */
10
+ export const isVersionTagRef = (ref) => versionTagPattern.test(ref);
5
11
  export const normalizeGitUri = (uri) => (uri.startsWith('git+') ? uri.slice('git+'.length) : uri);
6
12
  export const normalizeRemoteSourceUri = (source) => {
7
13
  if (source.uri !== undefined) {
@@ -32,9 +38,33 @@ export const redactSourceUriCredentials = (uri) => {
32
38
  export const redactEmbeddedSourceCredentials = (value) => value.replace(/((?:git\+)?[A-Za-z][A-Za-z0-9+.-]*:\/\/)[^/@\s]+@/gu, '$1REDACTED@');
33
39
  export const encodeSourceUri = (uri) => Buffer.from(redactSourceUriCredentials(uri), 'utf8').toString('base64url');
34
40
  export const encodeRemoteSource = (source) => encodeSourceUri(`${normalizeRemoteSourceUri(source)}#${source.ref ?? ''}`);
41
+ // The payload subpath a source selects, canonicalized so identical selections share one identity:
42
+ // an omitted path, `.`, `./`, and `foo/..` all normalize to the repository root ('').
43
+ const canonicalSourcePath = (path) => {
44
+ if (path === undefined)
45
+ return '';
46
+ const normalized = posix.normalize(path).replace(/\/+$/u, '');
47
+ return normalized === '.' ? '' : normalized;
48
+ };
49
+ /**
50
+ * A source's graph identity for dedup, visited sets, and layer ordering. It differs from
51
+ * {@link encodeRemoteSource} (the *checkout cache* key) in two deliberate ways:
52
+ *
53
+ * - It includes the canonicalized `path`, because two sources into different subpaths of one repo+ref
54
+ * are distinct layers — deduping them on the path-less cache key would silently drop one.
55
+ * - It keys on the *raw* normalized URI, credentials included, not the redacted cache key. Two entries
56
+ * for the same repo with different credentials are distinct configured sources; collapsing them
57
+ * (the cache key redacts credentials, so both share it) could drop a valid source for a broken one.
58
+ * This value is used only in ephemeral in-memory dedup/visited sets — never persisted or logged —
59
+ * so carrying credentials here is safe (the cache path and every user-facing display use the
60
+ * redacted forms).
61
+ */
62
+ export const encodeRemoteSourceSelection = (source) => `${normalizeRemoteSourceUri(source)}#${source.ref ?? ''} ${canonicalSourcePath(source.path)}`;
35
63
  /** Resolves the one cache root used by sync, settings, layer discovery, and setup bootstrap. */
36
64
  export const resolveRemoteRepositoryCacheRoot = (homeDirectory, cacheDirectory) => cacheDirectory ?? join(homeDirectory, '.agents', 'cache');
37
65
  export const createRemoteRepositoryCachePath = (homeDirectory, source, cacheDirectory) => join(resolveRemoteRepositoryCacheRoot(homeDirectory, cacheDirectory), 'repos', encodeRemoteSource(source));
66
+ /** The `.agents` payload root a source selects inside its materialized repository checkout. */
67
+ export const resolveSourcePayloadRoot = (repositoryPath, source) => source.path === undefined ? repositoryPath : resolveRemoteRepositorySubpath(repositoryPath, source.path);
38
68
  export const resolveRemoteRepositorySubpath = (repositoryPath, subpath = '') => {
39
69
  if (isAbsolute(subpath)) {
40
70
  throw new Error(`Remote repository path '${subpath}' must be relative.`);
@@ -1 +1 @@
1
- {"version":3,"file":"SourceCache.js","sourceRoot":"","sources":["../../src/sources/SourceCache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SourceCache.js","sourceRoot":"","sources":["../../src/sources/SourceCache.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOvE,+EAA+E;AAC/E,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,MAAS,EAC4B,EAAE,CAAC,MAAM,CAAC,GAAG,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC;AAElG,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;AAC5C,MAAM,iBAAiB,GAAG,uCAAuC,CAAC;AAElE,4FAA4F;AAC5F,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAW,EAAW,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEnH,mGAAmG;AACnG,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,GAAW,EAAW,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAErF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAElH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,MAA6B,EAAU,EAAE;IAChF,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,MAAM,CAAC,GAAG,CAAC;IACpB,CAAC;IAED,OAAO,0BAA0B,MAAM,CAAC,MAAM,MAAM,CAAC;AACvD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,MAA6B,EAAU,EAAE,CACjF,MAAM,CAAC,MAAM,KAAK,SAAS;IACzB,CAAC,CAAC,0BAA0B,CAAC,MAAM,CAAC,GAAG,CAAC;IACxC,CAAC,CAAC,UAAU,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC;AAEnF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,GAAW,EAAU,EAAE;IAChE,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACpD,MAAM,aAAa,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAE3C,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;QAEzC,IAAI,SAAS,CAAC,QAAQ,KAAK,EAAE,IAAI,SAAS,CAAC,QAAQ,KAAK,EAAE,EAAE,CAAC;YAC3D,OAAO,GAAG,CAAC;QACb,CAAC;QAED,SAAS,CAAC,QAAQ,GAAG,UAAU,CAAC;QAChC,SAAS,CAAC,QAAQ,GAAG,EAAE,CAAC;QACxB,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC,QAAQ,EAAE,EAAE,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC,OAAO,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC;IACvD,CAAC;AACH,CAAC,CAAC;AAEF,uFAAuF;AACvF,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,KAAa,EAAU,EAAE,CACvE,KAAK,CAAC,OAAO,CAAC,qDAAqD,EAAE,aAAa,CAAC,CAAC;AAEtF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,GAAW,EAAU,EAAE,CACrD,MAAM,CAAC,IAAI,CAAC,0BAA0B,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAE7E,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAA6B,EAAU,EAAE,CAC1E,eAAe,CAAC,GAAG,wBAAwB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AAE7E,kGAAkG;AAClG,sFAAsF;AACtF,MAAM,mBAAmB,GAAG,CAAC,IAAa,EAAU,EAAE;IACpD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAClC,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC9D,OAAO,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;AAC9C,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,MAA6B,EAAU,EAAE,CACnF,GAAG,wBAAwB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,GAAG,IAAI,EAAE,IAAI,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;AAEhG,gGAAgG;AAChG,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAAC,aAAqB,EAAE,cAAuB,EAAU,EAAE,CACzG,cAAc,IAAI,IAAI,CAAC,aAAa,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAC7C,aAAqB,EACrB,MAA6B,EAC7B,cAAuB,EACf,EAAE,CAAC,IAAI,CAAC,gCAAgC,CAAC,aAAa,EAAE,cAAc,CAAC,EAAE,OAAO,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;AAExH,+FAA+F;AAC/F,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,cAAsB,EAAE,MAA6B,EAAU,EAAE,CACxG,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,8BAA8B,CAAC,cAAc,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;AAE3G,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,cAAsB,EAAE,OAAO,GAAG,EAAE,EAAU,EAAE;IAC7F,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,2BAA2B,OAAO,qBAAqB,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,YAAY,GAAG,QAAQ,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAE5D,IAAI,YAAY,KAAK,IAAI,IAAI,YAAY,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QACxF,MAAM,IAAI,KAAK,CAAC,2BAA2B,OAAO,oCAAoC,CAAC,CAAC;IAC1F,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { SourceReference } from '../settings/Settings.js';
2
+ import type { RemoteSourceReference } from './SourceCache.js';
3
+ /** A `github:` catalog dependency, pinned to an immutable ref, attributed to its declaring catalog. */
4
+ export interface DeclaredRemoteSource {
5
+ readonly source: {
6
+ readonly github: string;
7
+ readonly ref: string;
8
+ };
9
+ /** Display name of the declaring catalog, for warnings and provenance. */
10
+ readonly declaredBy: string;
11
+ }
12
+ export interface DeclaredSourcesResult {
13
+ readonly sources: readonly DeclaredRemoteSource[];
14
+ readonly warnings: readonly string[];
15
+ }
16
+ /** The settings file a catalog payload root may carry: `settings.yml`, else `.agents/settings.yml`. */
17
+ export declare const resolveCatalogSettingsPath: (payloadRoot: string) => string | undefined;
18
+ /**
19
+ * Reads the `github:` catalog dependencies a cached catalog declares (content, never policy —
20
+ * OFTR-004.6.2). Transitive sources are deliberately the narrowest safe subset: a `github:`
21
+ * shorthand pinned to an immutable ref. A `uri:` source, a local `path:` source, a `path:` subpath,
22
+ * or a mutable ref is skipped with a warning naming the declaring catalog (OFTR-004.6.4,
23
+ * OFTR-004.6.5). Restricting to `github:` means every transitive fetch during `outfitter sync`
24
+ * passes through the private-catalog gate and can never select an arbitrary git transport (a `uri:`
25
+ * could name `ext::<cmd>` or a filesystem path); forbidding `path:` means a declaration can never
26
+ * escape its fetched checkout. An unreadable or invalid settings file skips the catalog's
27
+ * declarations without failing resolution (OFTR-004.6.8). (First-party default-catalog bootstrap
28
+ * fetches its declared closure without that interactive gate — see OFTR-004.6.10.) Broader
29
+ * transports remain future work under the trust model in #212.
30
+ */
31
+ export declare const readDeclaredRemoteSources: (payloadRoot: string, checkoutRoot: string, declaredBy: string) => DeclaredSourcesResult;
32
+ export interface TransitiveExpansionInput {
33
+ /** The directly configured sources, which seed the visited set and the first frontier. */
34
+ readonly directSources: readonly SourceReference[];
35
+ /** Maps a remote source to its cached checkout (clone) root, or undefined when it is not cached. */
36
+ readonly resolveCachedCheckoutRoot: (source: RemoteSourceReference) => string | undefined;
37
+ }
38
+ export interface TransitiveExpansionResult {
39
+ /** Newly discovered remote sources, breadth-first by depth then declaration order (OFTR-004.6.3). */
40
+ readonly sources: readonly DeclaredRemoteSource[];
41
+ /** Every accepted declaration, including duplicates suppressed from the resolved source graph. */
42
+ readonly declarations: readonly DeclaredRemoteSource[];
43
+ readonly warnings: readonly string[];
44
+ }
45
+ export declare const expandTransitiveSources: (input: TransitiveExpansionInput) => TransitiveExpansionResult;
@@ -0,0 +1,179 @@
1
+ // Expands the remote-source closure that cached catalogs declare in their own settings files.
2
+ import { existsSync, lstatSync, realpathSync, statSync } from 'node:fs';
3
+ import { isAbsolute, join, relative, sep } from 'node:path';
4
+ import { createSettingsLoadPlan, loadSettingsFiles } from '../settings/SettingsLoader.js';
5
+ import { encodeRemoteSourceSelection, formatRemoteSourceDisplay, isImmutableRef, isRemoteSource, resolveSourcePayloadRoot, } from './SourceCache.js';
6
+ // True when a filesystem entry exists at the path, *including a dangling symlink*. `existsSync`
7
+ // follows links and reports a dangling one as absent, which would silently drop (rather than warn
8
+ // about, per OFTR-004.6.8) a `settings.yml` committed as a broken symlink.
9
+ const entryExists = (path) => {
10
+ try {
11
+ lstatSync(path);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ };
18
+ /** The settings file a catalog payload root may carry: `settings.yml`, else `.agents/settings.yml`. */
19
+ export const resolveCatalogSettingsPath = (payloadRoot) => {
20
+ const rootSettingsPath = join(payloadRoot, 'settings.yml');
21
+ if (entryExists(rootSettingsPath))
22
+ return rootSettingsPath;
23
+ const nestedSettingsPath = join(payloadRoot, '.agents', 'settings.yml');
24
+ return entryExists(nestedSettingsPath) ? nestedSettingsPath : undefined;
25
+ };
26
+ /** Applies the safe-subset rules to one declared source, yielding a dependency or a skip reason. */
27
+ const classifyDeclaredSource = (declared, declaredBy) => {
28
+ if (!isRemoteSource(declared)) {
29
+ return {
30
+ skip: `Catalog '${declaredBy}' declares local path source '${declared.path}'; only 'github:' shorthands are resolved transitively, so it was skipped.`,
31
+ };
32
+ }
33
+ if (declared.github === undefined) {
34
+ return {
35
+ skip: `Catalog '${declaredBy}' declares source '${formatRemoteSourceDisplay(declared)}' as a 'uri:'; only 'github:' shorthands are resolved transitively, so it was skipped.`,
36
+ };
37
+ }
38
+ if (declared.path !== undefined) {
39
+ return {
40
+ skip: `Catalog '${declaredBy}' declares source '${formatRemoteSourceDisplay(declared)}' with a 'path:' subpath; transitive 'github:' sources must resolve their whole repository, so it was skipped.`,
41
+ };
42
+ }
43
+ if (declared.ref === undefined || !isImmutableRef(declared.ref)) {
44
+ return {
45
+ skip: `Catalog '${declaredBy}' declares source '${formatRemoteSourceDisplay(declared)}' without an immutable ref (a full commit SHA or version tag); it was skipped.`,
46
+ };
47
+ }
48
+ return { source: { github: declared.github, ref: declared.ref } };
49
+ };
50
+ /**
51
+ * Loads a cached catalog's own settings file, guarding against a hostile `settings.yml` committed as
52
+ * a symlink to a directory or a file outside the checkout (a `readFileSync` on a directory throws
53
+ * `EISDIR` and would crash resolution). Returns the parsed sources list, or a skip warning.
54
+ */
55
+ const loadCatalogSourceList = (payloadRoot, checkoutRoot, declaredBy) => {
56
+ const settingsPath = resolveCatalogSettingsPath(payloadRoot);
57
+ if (settingsPath === undefined)
58
+ return { sources: [] };
59
+ try {
60
+ if (!statSync(settingsPath).isFile())
61
+ throw new Error('settings.yml is not a regular file');
62
+ // Anchor containment to the *checkout* root (the clone Outfitter created), not the payload root:
63
+ // a `path:` selecting a symlinked subdirectory would make the payload root itself resolve
64
+ // outside the clone, so anchoring there would accept an external settings.yml as "contained".
65
+ const relativeToCheckout = relative(realpathSync(checkoutRoot), realpathSync(settingsPath));
66
+ // Precise containment: a leading `..` segment (not a filename that merely starts with `..`, such
67
+ // as a directory named `..catalog`) or an absolute result means the settings escaped the checkout.
68
+ if (relativeToCheckout === '..' || relativeToCheckout.startsWith(`..${sep}`) || isAbsolute(relativeToCheckout)) {
69
+ throw new Error('settings.yml resolves outside the checkout');
70
+ }
71
+ // Known-accepted TOCTOU: the checkout could be swapped between this stat/realpath check and the
72
+ // read below. The checkout is a pinned immutable git ref that only Outfitter writes, and racing
73
+ // local-filesystem writes already imply broader access, so closing this window is not warranted.
74
+ const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'remote', path: settingsPath }]));
75
+ if (loaded.issues.length > 0) {
76
+ return {
77
+ warning: `Catalog '${declaredBy}' declares invalid settings at '${settingsPath}'; its sources were skipped.`,
78
+ };
79
+ }
80
+ return { sources: loaded.files[0].settings.sources ?? [] };
81
+ }
82
+ catch {
83
+ return {
84
+ warning: `Catalog '${declaredBy}' has an unreadable settings.yml (not a regular file inside the checkout); its sources were skipped.`,
85
+ };
86
+ }
87
+ };
88
+ /**
89
+ * Reads the `github:` catalog dependencies a cached catalog declares (content, never policy —
90
+ * OFTR-004.6.2). Transitive sources are deliberately the narrowest safe subset: a `github:`
91
+ * shorthand pinned to an immutable ref. A `uri:` source, a local `path:` source, a `path:` subpath,
92
+ * or a mutable ref is skipped with a warning naming the declaring catalog (OFTR-004.6.4,
93
+ * OFTR-004.6.5). Restricting to `github:` means every transitive fetch during `outfitter sync`
94
+ * passes through the private-catalog gate and can never select an arbitrary git transport (a `uri:`
95
+ * could name `ext::<cmd>` or a filesystem path); forbidding `path:` means a declaration can never
96
+ * escape its fetched checkout. An unreadable or invalid settings file skips the catalog's
97
+ * declarations without failing resolution (OFTR-004.6.8). (First-party default-catalog bootstrap
98
+ * fetches its declared closure without that interactive gate — see OFTR-004.6.10.) Broader
99
+ * transports remain future work under the trust model in #212.
100
+ */
101
+ export const readDeclaredRemoteSources = (payloadRoot, checkoutRoot, declaredBy) => {
102
+ const loaded = loadCatalogSourceList(payloadRoot, checkoutRoot, declaredBy);
103
+ if ('warning' in loaded)
104
+ return { sources: [], warnings: [loaded.warning] };
105
+ const sources = [];
106
+ const warnings = [];
107
+ for (const declared of loaded.sources) {
108
+ const classified = classifyDeclaredSource(declared, declaredBy);
109
+ if ('skip' in classified)
110
+ warnings.push(classified.skip);
111
+ else
112
+ sources.push({ source: classified.source, declaredBy });
113
+ }
114
+ return { sources, warnings };
115
+ };
116
+ /**
117
+ * Walks declared sources breadth-first from the directly configured sources over the local cache.
118
+ * A source already visited — directly configured or declared by an earlier catalog — is never
119
+ * expanded again, so dependency cycles terminate (OFTR-004.6.6). An uncached source stays in the
120
+ * result (the caller reports it) but its own declarations are unknowable until it is synced.
121
+ */
122
+ // Reads one parent's cached checkout and returns the declarations it contributes. Returns `undefined`
123
+ // (skip this parent) when it is uncached or its `path:` is absolute/escaping — a throw here would
124
+ // otherwise crash resolution; only a directly configured source can carry such a path.
125
+ const declarationsFromParent = (parent, resolveCachedCheckoutRoot) => {
126
+ const checkoutRoot = resolveCachedCheckoutRoot(parent);
127
+ if (checkoutRoot === undefined)
128
+ return undefined;
129
+ let payloadRoot;
130
+ try {
131
+ payloadRoot = resolveSourcePayloadRoot(checkoutRoot, parent);
132
+ }
133
+ catch {
134
+ return undefined;
135
+ }
136
+ if (!existsSync(payloadRoot))
137
+ return undefined;
138
+ return readDeclaredRemoteSources(payloadRoot, checkoutRoot, formatRemoteSourceDisplay(parent));
139
+ };
140
+ export const expandTransitiveSources = (input) => {
141
+ const sources = [];
142
+ const declarations = [];
143
+ const warnings = [];
144
+ const visited = new Set();
145
+ // Seed the visited set with each direct source's path-aware identity so a directly configured
146
+ // subpath source does not suppress a transitive whole-repository source of the same repo+ref, and
147
+ // so an exact duplicate direct source is expanded only once (OFTR-004.6.6).
148
+ let frontier = [];
149
+ for (const direct of input.directSources) {
150
+ if (!isRemoteSource(direct))
151
+ continue;
152
+ const key = encodeRemoteSourceSelection(direct);
153
+ if (visited.has(key))
154
+ continue;
155
+ visited.add(key);
156
+ frontier.push(direct);
157
+ }
158
+ while (frontier.length > 0) {
159
+ const next = [];
160
+ for (const parent of frontier) {
161
+ const declared = declarationsFromParent(parent, input.resolveCachedCheckoutRoot);
162
+ if (declared === undefined)
163
+ continue;
164
+ warnings.push(...declared.warnings);
165
+ for (const entry of declared.sources) {
166
+ declarations.push(entry);
167
+ const key = encodeRemoteSourceSelection(entry.source);
168
+ if (visited.has(key))
169
+ continue;
170
+ visited.add(key);
171
+ sources.push(entry);
172
+ next.push(entry.source);
173
+ }
174
+ }
175
+ frontier = next;
176
+ }
177
+ return { sources, declarations, warnings };
178
+ };
179
+ //# sourceMappingURL=TransitiveSources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TransitiveSources.js","sourceRoot":"","sources":["../../src/sources/TransitiveSources.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { AgentLaunchPlan } from '../projection/Projection.js';
2
+ import type { Harness } from '../settings/Settings.js';
3
+ export interface SystemExtensionHarnessHook {
4
+ readonly extensions?: readonly string[];
5
+ readonly env?: Readonly<Record<string, string>>;
6
+ }
7
+ export interface SystemExtensionHook {
8
+ readonly filePath: string;
9
+ readonly name: string;
10
+ readonly harnesses: Partial<Readonly<Record<Harness, SystemExtensionHarnessHook>>>;
11
+ }
12
+ export interface SystemExtensionHookSource {
13
+ readonly directory: string;
14
+ readonly stamp: string;
15
+ }
16
+ export interface LoadedSystemExtensionHooks {
17
+ readonly hooks: readonly SystemExtensionHook[];
18
+ readonly source?: SystemExtensionHookSource;
19
+ }
20
+ export interface AttachedSystemExtensionHooks {
21
+ readonly launch: AgentLaunchPlan;
22
+ readonly warnings: readonly string[];
23
+ }
24
+ export interface SystemExtensionHookDiscoveryInput {
25
+ readonly environment?: NodeJS.ProcessEnv;
26
+ readonly platform?: NodeJS.Platform;
27
+ }
28
+ export declare const resolveSystemExtensionHookSource: (input?: SystemExtensionHookDiscoveryInput) => SystemExtensionHookSource | undefined;
29
+ export declare const readSystemExtensionHooks: (input?: SystemExtensionHookDiscoveryInput) => LoadedSystemExtensionHooks;
30
+ /**
31
+ * Prepends every system Pi extension after projection, including for RPC/print launches. Hook env
32
+ * stays beneath the projected plan env so it cannot replace Outfitter's runtime/session paths.
33
+ */
34
+ export declare const attachSystemExtensionHooks: (plan: AgentLaunchPlan, loaded?: LoadedSystemExtensionHooks) => AttachedSystemExtensionHooks;
@@ -0,0 +1,190 @@
1
+ // Reads root-owned system extension hooks for post-projection launch-plan mutation. This is a
2
+ // launcher-scope mechanism, not a managed configuration scope: Pi never reads these files, so they
3
+ // do not change Pi's configuration resolution order. A session can still bypass the launcher with
4
+ // the bundled Pi binary directly or point OUTFITTER_SYSTEM_DIR at an empty directory. The honest
5
+ // guarantee is that collection is on by default and the organization owns the file naming the
6
+ // collector, not that a session cannot turn collection off.
7
+ //
8
+ // Hook documents fail closed because only an operator can create files in the normal root-owned
9
+ // locations. A malformed file is therefore an operator error caught on a canary boot; failing open
10
+ // would silently run the fleet without collection, which downstream verifiers must classify as
11
+ // unattested rather than clean. Hook environment also denies Node and native dynamic-loader
12
+ // controls: they could execute code before Pi starts and change what runs rather than what observes.
13
+ // Symlinks are resolved to physical paths and dangling links are fatal. Ownership and mode checks
14
+ // stay with deployment policy because supported paths include platform-specific package stores and
15
+ // the explicit development override, which do not share one portable owner or permission model.
16
+ import { lstatSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
17
+ import { isAbsolute, join } from 'node:path';
18
+ import { validateSchema } from '../validation/SchemaValidator.js';
19
+ import { parseYamlDocument } from '../validation/YamlDocument.js';
20
+ const linuxSystemDirectory = '/etc/outfitter/system.d';
21
+ const macosSystemDirectory = '/Library/Application Support/Outfitter/system.d';
22
+ const protectedPiEnvironmentVariables = new Set(['PI_CODING_AGENT_DIR', 'PI_CODING_AGENT_SESSION_DIR']);
23
+ const runtimeControlEnvironmentVariables = new Set([
24
+ 'LD_AUDIT',
25
+ 'LD_LIBRARY_PATH',
26
+ 'LD_PRELOAD',
27
+ 'NODE_OPTIONS',
28
+ 'NODE_REPL_EXTERNAL_MODULE',
29
+ // Node reads this before startup and OpenSSL configuration can activate an
30
+ // arbitrary native provider, so it executes code exactly like the loaders above.
31
+ 'OPENSSL_CONF',
32
+ ]);
33
+ export const resolveSystemExtensionHookSource = (input = {}) => {
34
+ const environment = input.environment ?? process.env;
35
+ const override = environment.OUTFITTER_SYSTEM_DIR;
36
+ if (override !== undefined)
37
+ return { directory: override, stamp: `env-override:${override}` };
38
+ switch (input.platform ?? process.platform) {
39
+ case 'darwin':
40
+ return { directory: macosSystemDirectory, stamp: macosSystemDirectory };
41
+ case 'linux':
42
+ return { directory: linuxSystemDirectory, stamp: linuxSystemDirectory };
43
+ default:
44
+ return undefined;
45
+ }
46
+ };
47
+ const formatValidationIssues = (filePath, issues) => issues.map((issue) => `${filePath}#${issue.path} ${issue.message}`).join('; ');
48
+ const resolvePhysicalPath = (path, errorMessage) => {
49
+ try {
50
+ return realpathSync(path);
51
+ }
52
+ catch (error) {
53
+ throw new Error(`${errorMessage}: ${String(error)}`, { cause: error });
54
+ }
55
+ };
56
+ const resolveExtensionPaths = (document, filePath) => {
57
+ const harnesses = {};
58
+ for (const [harness, hook] of Object.entries(document.harnesses)) {
59
+ if (hook.extensions === undefined) {
60
+ harnesses[harness] = hook;
61
+ continue;
62
+ }
63
+ harnesses[harness] = {
64
+ ...hook,
65
+ extensions: hook.extensions.map((extensionPath) => {
66
+ if (!isAbsolute(extensionPath) || lstatSync(extensionPath, { throwIfNoEntry: false }) === undefined) {
67
+ throw new Error(`Invalid system extension hook '${filePath}': extension path does not exist: ${extensionPath}`);
68
+ }
69
+ return resolvePhysicalPath(extensionPath, `Invalid system extension hook '${filePath}': extension path cannot be resolved: ${extensionPath}`);
70
+ }),
71
+ };
72
+ }
73
+ return { ...document, harnesses };
74
+ };
75
+ const assertNoProtectedPiEnvironment = (document, filePath) => {
76
+ for (const hook of Object.values(document.harnesses)) {
77
+ for (const name of Object.keys(hook.env ?? {})) {
78
+ if (protectedPiEnvironmentVariables.has(name.toUpperCase())) {
79
+ throw new Error(`Invalid system extension hook '${filePath}': environment variable '${name}' is reserved by Outfitter.`);
80
+ }
81
+ }
82
+ }
83
+ };
84
+ const assertNoRuntimeControlEnvironment = (document, filePath) => {
85
+ for (const hook of Object.values(document.harnesses)) {
86
+ for (const name of Object.keys(hook.env ?? {})) {
87
+ const normalizedName = name.toUpperCase();
88
+ if (runtimeControlEnvironmentVariables.has(normalizedName) || normalizedName.startsWith('DYLD_')) {
89
+ throw new Error(`Invalid system extension hook '${filePath}': environment variable '${name}' controls process loading and is forbidden.`);
90
+ }
91
+ }
92
+ }
93
+ };
94
+ const readSystemExtensionHook = (filePath) => {
95
+ if (lstatSync(filePath, { throwIfNoEntry: false }) === undefined) {
96
+ /* v8 ignore next -- the path was returned by the immediately preceding readdirSync call. */
97
+ throw new Error(`System extension hook '${filePath}' disappeared while loading.`);
98
+ }
99
+ const physicalFilePath = resolvePhysicalPath(filePath, `System extension hook '${filePath}' cannot be resolved`);
100
+ let content;
101
+ try {
102
+ content = readFileSync(physicalFilePath, 'utf8');
103
+ }
104
+ catch (error) {
105
+ throw new Error(`System extension hook '${physicalFilePath}' is unreadable: ${String(error)}`, { cause: error });
106
+ }
107
+ const parsed = parseYamlDocument(content, physicalFilePath);
108
+ if (!parsed.ok) {
109
+ throw new Error(`Invalid system extension hook '${physicalFilePath}': ${parsed.issue.message}`);
110
+ }
111
+ const validation = validateSchema('system-extension-hook', parsed.document);
112
+ if (!validation.valid) {
113
+ throw new Error(`Invalid system extension hook: ${formatValidationIssues(physicalFilePath, validation.issues)}`);
114
+ }
115
+ const document = parsed.document;
116
+ assertNoProtectedPiEnvironment(document, physicalFilePath);
117
+ assertNoRuntimeControlEnvironment(document, physicalFilePath);
118
+ return { filePath: physicalFilePath, ...resolveExtensionPaths(document, physicalFilePath) };
119
+ };
120
+ const assertNoEnvironmentCollisions = (hooks) => {
121
+ const owners = new Map();
122
+ for (const hook of hooks) {
123
+ for (const harness of ['pi', 'claude', 'codex']) {
124
+ for (const name of Object.keys(hook.harnesses[harness]?.env ?? {})) {
125
+ const key = `${harness}:${name}`;
126
+ const owner = owners.get(key);
127
+ if (owner !== undefined) {
128
+ throw new Error(`Invalid system extension hooks: environment '${name}' for '${harness}' is declared by both '${owner}' and '${hook.filePath}'.`);
129
+ }
130
+ owners.set(key, hook.filePath);
131
+ }
132
+ }
133
+ }
134
+ };
135
+ export const readSystemExtensionHooks = (input = {}) => {
136
+ const source = resolveSystemExtensionHookSource(input);
137
+ if (source === undefined)
138
+ return { hooks: [] };
139
+ const sourceEntry = lstatSync(source.directory, { throwIfNoEntry: false });
140
+ if (sourceEntry === undefined)
141
+ return { hooks: [], source };
142
+ const physicalDirectory = resolvePhysicalPath(source.directory, `System extension hook source '${source.directory}' cannot be resolved`);
143
+ const sourceStat = statSync(physicalDirectory);
144
+ if (!sourceStat.isDirectory()) {
145
+ throw new Error(`System extension hook source '${physicalDirectory}' is not a directory.`);
146
+ }
147
+ const physicalSource = {
148
+ directory: physicalDirectory,
149
+ stamp: `${source.stamp.slice(0, source.stamp.length - source.directory.length)}${physicalDirectory}`,
150
+ };
151
+ const filePaths = readdirSync(physicalDirectory)
152
+ .filter((name) => name.endsWith('.yml'))
153
+ .sort()
154
+ .map((name) => join(physicalDirectory, name));
155
+ const hooks = filePaths.map(readSystemExtensionHook);
156
+ assertNoEnvironmentCollisions(hooks);
157
+ return { hooks, source: physicalSource };
158
+ };
159
+ const unsupportedHarnessWarnings = (hooks) => hooks.flatMap((hook) => ['claude', 'codex']
160
+ .filter((harness) => hook.harnesses[harness] !== undefined)
161
+ .map((harness) => `warning: System extension hook '${hook.name}' configures unsupported harness '${harness}'; ignoring it.`));
162
+ /**
163
+ * Prepends every system Pi extension after projection, including for RPC/print launches. Hook env
164
+ * stays beneath the projected plan env so it cannot replace Outfitter's runtime/session paths.
165
+ */
166
+ export const attachSystemExtensionHooks = (plan, loaded = readSystemExtensionHooks()) => {
167
+ const warnings = unsupportedHarnessWarnings(loaded.hooks);
168
+ if (loaded.source === undefined)
169
+ return { launch: plan, warnings };
170
+ const piHooks = loaded.hooks.flatMap((hook) => (hook.harnesses.pi === undefined ? [] : [hook.harnesses.pi]));
171
+ const extensionArgs = plan.command === 'pi'
172
+ ? piHooks.flatMap((hook) => (hook.extensions ?? []).flatMap((path) => ['--extension', path]))
173
+ : [];
174
+ const hookEnvironment = plan.command === 'pi'
175
+ ? piHooks.reduce((environment, hook) => ({ ...environment, ...hook.env }), {})
176
+ : {};
177
+ return {
178
+ launch: {
179
+ ...plan,
180
+ args: [...extensionArgs, ...plan.args],
181
+ env: {
182
+ ...hookEnvironment,
183
+ OUTFITTER_SYSTEM_HOOK_SOURCE: loaded.source.stamp,
184
+ ...plan.env,
185
+ },
186
+ },
187
+ warnings,
188
+ };
189
+ };
190
+ //# sourceMappingURL=SystemExtensionHook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SystemExtensionHook.js","sourceRoot":"","sources":["../../src/system/SystemExtensionHook.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export type SchemaName = 'settings' | 'agent';
1
+ export type SchemaName = 'settings' | 'agent' | 'system-extension-hook';
2
2
  export interface ValidationIssue {
3
3
  readonly path: string;
4
4
  readonly message: string;
@@ -4,10 +4,12 @@ import { Ajv2020 } from 'ajv/dist/2020.js';
4
4
  const readSchema = (schemaFileName) => JSON.parse(readFileSync(new URL(`../schemas/${schemaFileName}`, import.meta.url), 'utf8'));
5
5
  const settingsSchema = readSchema('settings.schema.json');
6
6
  const agentSchema = readSchema('agent.schema.json');
7
+ const systemExtensionHookSchema = readSchema('system-extension-hook.schema.json');
7
8
  const ajv = new Ajv2020({ allErrors: true });
8
9
  const validators = {
9
10
  settings: ajv.compile(settingsSchema),
10
11
  agent: ajv.compile(agentSchema),
12
+ 'system-extension-hook': ajv.compile(systemExtensionHookSchema),
11
13
  };
12
14
  export const createValidationResult = (issues) => ({
13
15
  valid: issues.length === 0,
@@ -1 +1 @@
1
- {"version":3,"file":"SchemaValidator.js","sourceRoot":"","sources":["../../src/validation/SchemaValidator.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAc3C,MAAM,UAAU,GAAG,CAAC,cAAsB,EAAW,EAAE,CACrD,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,cAAc,cAAc,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7F,MAAM,cAAc,GAAG,UAAU,CAAC,sBAAsB,CAAC,CAAC;AAC1D,MAAM,WAAW,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAC;AAEpD,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAE7C,MAAM,UAAU,GAAyC;IACvD,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,cAA2B,CAAC;IAClD,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,WAAwB,CAAC;CAC7C,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,MAAkC,EAAoB,EAAE,CAAC,CAAC;IAC/F,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;IAC1B,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,UAAsB,EAAE,QAAiB,EAAoB,EAAE;IAC5F,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IAExC,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvB,OAAO,sBAAsB,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,sBAAsB,CAAE,QAAQ,CAAC,MAAiC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC;AACjG,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,KAAkB,EAAmB,EAAE,CAAC,CAAC;IAC/D,IAAI,EAAE,KAAK,CAAC,YAAY,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY;IAC1D,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;CAC/B,CAAC,CAAC"}
1
+ {"version":3,"file":"SchemaValidator.js","sourceRoot":"","sources":["../../src/validation/SchemaValidator.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAc3C,MAAM,UAAU,GAAG,CAAC,cAAsB,EAAW,EAAE,CACrD,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,cAAc,cAAc,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7F,MAAM,cAAc,GAAG,UAAU,CAAC,sBAAsB,CAAC,CAAC;AAC1D,MAAM,WAAW,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAC;AACpD,MAAM,yBAAyB,GAAG,UAAU,CAAC,mCAAmC,CAAC,CAAC;AAElF,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAE7C,MAAM,UAAU,GAAyC;IACvD,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,cAA2B,CAAC;IAClD,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,WAAwB,CAAC;IAC5C,uBAAuB,EAAE,GAAG,CAAC,OAAO,CAAC,yBAAsC,CAAC;CAC7E,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,MAAkC,EAAoB,EAAE,CAAC,CAAC;IAC/F,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;IAC1B,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,UAAsB,EAAE,QAAiB,EAAoB,EAAE;IAC5F,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IAExC,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvB,OAAO,sBAAsB,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,sBAAsB,CAAE,QAAQ,CAAC,MAAiC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC;AACjG,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,KAAkB,EAAmB,EAAE,CAAC,CAAC;IAC/D,IAAI,EAAE,KAAK,CAAC,YAAY,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY;IAC1D,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;CAC/B,CAAC,CAAC"}
@@ -26,7 +26,19 @@ The user-facing state update lifecycle is:
26
26
  ## Current behavior
27
27
 
28
28
  - Projections remain temporary and reproducible by default.
29
- - Outfitter does not do generic post-run copy-back or JSON/YAML merge-back.
29
+ - Outfitter does not do generic post-run copy-back or JSON/YAML merge-back. The Pi and Claude
30
+ adapters have narrow credential and session bridges as exceptions because the current launch flow does not
31
+ connect those files to durable storage through materialized declared-state symlinks. Pi declares
32
+ `auth.json` and `models.json` with a `symlink` default, but currently copies them from the durable
33
+ agent directory into the projection before launch and back from the projection afterward. Claude
34
+ similarly copies `.credentials.json` from `~/.claude` into the projection and copies a changed
35
+ file back afterward. Claude's separate `~/.claude.json` sits outside `~/.claude`, so it cannot be
36
+ symlinked with the rest of the config directory; Outfitter seeds a narrow whitelist into the
37
+ projected file and merges only `oauthAccount` back rather than copying machine-local state
38
+ wholesale. Claude's `CLAUDE_CONFIG_DIR` also redirects session transcripts into the projection,
39
+ so Outfitter seeds only the current working directory's `projects/<slug>/` before launch and
40
+ atomically merges projected session files from every slug back afterward. This per-file merge
41
+ never deletes durable history and runs after both normal and thrown launcher exits.
30
42
  - Persistent state is represented by symlinking a projection path to the native CLI fallback path.
31
43
  - Adapters may generate a concrete runtime file for a declared state path when they need deterministic launch-time reconciliation. For example, the Pi adapter can generate a transformed `settings.json` that removes native `packages` entries already supplied by composition-controlled extensions, and then mark that declared path as `discard` for write detection during the run.
32
44
  - Every adapter-declared state path has a resolved strategy before launch: settings overrides win, otherwise the adapter `default_strategy` is used, except for adapter-generated reconciliation files that are intentionally treated as discarded runtime files.
@@ -181,7 +193,7 @@ Settings may override persistence by mapping adapter-declared paths to strategy
181
193
 
182
194
  ```yaml
183
195
  state_persistence:
184
- auth.json: symlink
196
+ auth.json: symlink # Intended policy; Pi's current launch flow uses its explicit copy bridge.
185
197
  settings.json: symlink
186
198
  plugins/: symlink
187
199
  cache/: discard
@@ -198,7 +210,7 @@ Functional examples:
198
210
  ```yaml
199
211
  # Persist logins and settings, but make caches and sessions run-local.
200
212
  state_persistence:
201
- auth.json: symlink
213
+ auth.json: symlink # Intended policy; Pi's current launch flow uses its explicit copy bridge.
202
214
  settings.json: symlink
203
215
  cache/: discard
204
216
  sessions/: discard