@hasna/skills 0.10.70 → 0.10.72

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.
@@ -1,3 +1,4 @@
1
+ import { type NativeDiscoveryWitnessTolerance } from "./native-discovery-witness-change.js";
1
2
  /** Recursive path/type membership only; source-file hashes bind reviewed bytes. */
2
3
  export interface DiscoveryDirectory {
3
4
  path: string;
@@ -10,8 +11,12 @@ export interface DiscoveryDirectoryProjection extends DiscoveryDirectory {
10
11
  * reviewed inactive roots. It never replaces a historical full witness. */
11
12
  export declare function captureDiscoveryDirectoryProjection(paths: string[], omittedRoots: readonly string[]): DiscoveryDirectoryProjection[];
12
13
  /** Compare a planner-captured inventory after removing only exact inert graphs.
13
- * The stored rows must reproduce their original digest before projection. */
14
- export declare function verifyDiscoveryDirectoryProjection(directories: DiscoveryDirectoryProjection[], omittedRoots: readonly string[]): void;
14
+ * The stored rows must reproduce their original digest before projection. A
15
+ * membership change is a NativeDiscoveryWitnessChange: thrown, unless the live
16
+ * Codex prompt hook's tolerance accepts it. Invalid inventories always throw. */
17
+ export declare function verifyDiscoveryDirectoryProjection(directories: DiscoveryDirectoryProjection[], omittedRoots: readonly string[], tolerance?: NativeDiscoveryWitnessTolerance): void;
15
18
  /** Read-only capture; callers must review both membership and source bytes. */
16
19
  export declare function captureDiscoveryDirectories(paths: string[], excludedRoots?: readonly string[]): DiscoveryDirectory[];
17
- export declare function verifyDiscoveryDirectories(directories: DiscoveryDirectory[], excludedRoots?: readonly string[]): void;
20
+ /** A membership change is a NativeDiscoveryWitnessChange: thrown, unless the
21
+ * live Codex prompt hook's tolerance accepts it. Invalid collections always throw. */
22
+ export declare function verifyDiscoveryDirectories(directories: DiscoveryDirectory[], excludedRoots?: readonly string[], tolerance?: NativeDiscoveryWitnessTolerance): void;
@@ -1,6 +1,7 @@
1
1
  import type { IntegrationAgent } from "./agent-adapters.js";
2
2
  import { type DiscoveryDirectory } from "./agent-discovery-directories.js";
3
3
  import { type CodexReviewedHookSource } from "./codex-hook-mcp-dependencies.js";
4
+ import { type NativeDiscoveryWitnessTolerance } from "./native-discovery-witness-change.js";
4
5
  import { type ManagedPluginRegistrationWitness } from "./plugin-discovery.js";
5
6
  import { type CodexHookDiscoveryRecovery } from "./codex-hook-discovery-recovery.js";
6
7
  import { type CodexPluginSkillControl, type CodexPluginSourceInput } from "./codex-plugin-skill-controls.js";
@@ -11,7 +12,7 @@ export interface DiscoverySource {
11
12
  reviewArtifact?: RetainedReviewArtifact;
12
13
  path: string;
13
14
  sha256: string | null;
14
- hashMode?: "bytes" | "path-bytes" | "claude-plugin-manifest-v1" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3" | "claude-settings-v4" | "claude-marketplace-registry-v2" | "codex-settings-v1" | "codex-settings-v2" | "codex-settings-v3" | "codex-settings-v4" | "codex-settings-v5" | "codex-settings-v6" | "executable-v1" | "sumi-settings-v1" | "claude-marketplace-entry-v1";
15
+ hashMode?: "bytes" | "path-bytes" | "claude-plugin-orphaned-at-v1" | "claude-plugin-manifest-v1" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3" | "claude-settings-v4" | "claude-marketplace-registry-v2" | "codex-settings-v1" | "codex-settings-v2" | "codex-settings-v3" | "codex-settings-v4" | "codex-settings-v5" | "codex-settings-v6" | "executable-v1" | "sumi-settings-v1" | "claude-marketplace-entry-v1";
15
16
  hookSources?: CodexReviewedHookSource[];
16
17
  managedPlugins?: ManagedPluginRegistrationWitness[];
17
18
  format?: "json" | "toml" | "yaml";
@@ -99,15 +100,19 @@ export declare function captureRetiredCodexDiscovery(binding: AgentDiscoveryBind
99
100
  export declare function codexSemanticCacheSourceRole(source: DiscoverySource, cacheRoot: string, parents: readonly string[], hookParents?: readonly string[]): boolean;
100
101
  export declare function codexSemanticCacheDirectoryRole(path: string, cacheRoot: string): boolean;
101
102
  export declare function assertCodexSemanticCacheHookSafety(cacheRoot: string, parents: readonly string[], configPath: string, hookRootAlias?: CodexSemanticCacheDiscovery["hookRootAlias"]): void;
103
+ /** Source-byte and directory-membership drift are NativeDiscoveryWitnessChange
104
+ * errors. Only the live Codex prompt hook passes `witnessTolerance`; a change it
105
+ * accepts as a vendor refresh is collected and the remaining checks still run.
106
+ * Every other change and every other failure throws as before. */
102
107
  export declare function verifyAgentDiscovery(binding: AgentDiscoveryBinding, codexRecovery?: CodexHookDiscoveryRecovery, reviewedRootAliases?: readonly {
103
108
  agent: IntegrationAgent;
104
109
  alias: string;
105
110
  target: string;
106
- }[]): void;
111
+ }[], witnessTolerance?: NativeDiscoveryWitnessTolerance): void;
107
112
  export declare function rebindAgentDiscovery(binding: AgentDiscoveryBinding, changes: Map<string, string>): AgentDiscoveryBinding;
108
113
  /** Explicit streaming integrity witness. Never read an executable as text or execute it. */
109
114
  export declare function captureDiscoveryExecutableSources(paths: string[]): DiscoverySource[];
110
- /** Capture full byte witnesses for reviewed source or executable files; never import or execute them. */
115
+ /** Capture exact source bytes, or typed Claude cache marker presence; never execute them. */
111
116
  export declare function captureDiscoveryByteSources(paths: string[]): DiscoverySource[];
112
117
  /** Capture executable/source path identity, including links and absent targets. */
113
118
  export declare function captureDiscoveryPathSources(paths: string[]): DiscoverySource[];
@@ -120,13 +125,24 @@ export declare function assertProjectDiscovery(agent: IntegrationAgent, director
120
125
  export declare function agentDiscoveryConfigPath(home: string, agent: IntegrationAgent): string;
121
126
  /** Re-derive an automatic binding and compare it with the stored one. A
122
127
  * mismatch is drift, except when the only difference is a runtime this process
123
- * cannot resolve: then it is DISCOVERY_ROOT_UNRESOLVED, which stays blocking. */
128
+ * cannot resolve: then it is DISCOVERY_ROOT_UNRESOLVED, which stays blocking.
129
+ * Only the live Codex prompt hook passes `witnessTolerance`: when roots, method
130
+ * and every source and directory path are unchanged, only witness digests differ
131
+ * and the tolerance accepts every one of them, they are collected instead of
132
+ * refusing. */
124
133
  export declare function verifyAutomaticDiscoveryClosure(binding: AgentDiscoveryBinding, options: {
125
134
  home: string;
126
135
  canonical: (path: string) => string;
127
136
  change: string;
128
137
  executable?: DiscoveryExecutable;
138
+ witnessTolerance?: NativeDiscoveryWitnessTolerance;
129
139
  }): void;
140
+ /** True only when the changed Codex config.toml differs from its reviewed
141
+ * witness by appended vendor disable rows and/or vendor plugin disables:
142
+ * removing those entries or restoring a formerly enabled vendor flag reproduces
143
+ * the stored digest. Witness modes
144
+ * without a replacement digest (V6, artifacts, managed registries) stay strict. */
145
+ export declare function codexVendorConfigChangeOnly(source: DiscoverySource, cache: string): boolean;
130
146
  /** `executable` is the agent's runtime: omitted, it is resolved through PATH;
131
147
  * a value (or null for none) is used as given, so verification never depends
132
148
  * on the caller's PATH. */
@@ -2,6 +2,7 @@ import { type CodexCorpusWriteOptions } from "./codex-corpus-write.js";
2
2
  import { type ReviewedCodexSkillDenial } from "./codex-plugin-skill-controls.js";
3
3
  import { type CodexNativeHookEnvelope, type ProcessInspector, type NativePolicyHelperRunner } from "./codex-native-skill-policy.js";
4
4
  import { type CodexNativeSkillCatalog } from "./codex-native-skill-catalog.js";
5
+ import type { NativeDiscoveryWitnessKind } from "./native-discovery-witness-change.js";
5
6
  import { type DiscoveryExecutable, type AgentDiscoveryBinding, type ReviewedDiscoveryInputs } from "./agent-discovery.js";
6
7
  import { type IntegrationAgent } from "./agent-adapters.js";
7
8
  import type { CodexHookDiscoveryRecovery } from "./codex-hook-discovery-recovery.js";
@@ -279,15 +280,52 @@ interface ManagedBridgeOptions {
279
280
  profileId?: string;
280
281
  codexDiscoveryRecovery?: CodexHookDiscoveryRecovery;
281
282
  codexNativePolicy?: CodexNativePolicyGuardInput;
283
+ /** Live Codex prompt hook only. Exempt vendor Codex plugin-cache copies that
284
+ * Codex configuration provably disables, and tolerate, with a warning, the
285
+ * discovery-witness changes a vendor plugin refresh provably makes. Enabled
286
+ * vendor copies and every other change still refuse. Install, trust, migrate
287
+ * and sync never pass it. */
288
+ codexPromptHook?: boolean;
289
+ }
290
+ /** Result of a guard call; both lists are empty unless the caller opted in.
291
+ * `codexInertVendorCopies` holds the canonical skill directories exempted as
292
+ * disabled vendor copies; `codexDiscoveryChanges` the witnesses tolerated as a
293
+ * vendor plugin refresh. */
294
+ export interface ManagedBridgeReport {
295
+ codexInertVendorCopies: string[];
296
+ codexDiscoveryChanges: Array<{
297
+ kind: NativeDiscoveryWitnessKind;
298
+ path: string | null;
299
+ }>;
282
300
  }
283
301
  /** Package-classified installed-plugin content below the Codex plugin cache that
284
302
  * a verified restricted policy cannot load. Classification is the existing plugin
285
303
  * manifest identity and real-path binding, never a path prefix; user, repo and
286
304
  * project copies, unclassified cache files and any allowed host path refuse. */
287
305
  export declare function isInertCodexPluginCacheCopy(entry: NativeSkillEntry, cache: string, allowedHostPaths: readonly string[], read: (path: string) => string): boolean;
306
+ /** A vendor plugin-cache copy that Codex configuration provably disables, so its
307
+ * payload never loads natively. Vendor means: classified installed-plugin content
308
+ * in an allowlisted OpenAI marketplace (exact names), with the native remote
309
+ * install receipt for remote marketplaces, no Skills-managed or Hasna marker,
310
+ * and a plugin whose every cached version passes the fail-closed refresh check
311
+ * (codexVendorPluginRefreshSafe: one Codex manifest with only known keys, no
312
+ * hooks, MCP, command, agent or extension declaration, no other manifest
313
+ * location, no link and no skill hidden from the inventory or nested in
314
+ * another). Classification reads only the Codex manifest, so without that check
315
+ * a version could load payloads no rule here covers. Disabled means: a `[[skills.config]]`
316
+ * rule names its exact SKILL.md path or its plugin-qualified name with
317
+ * `enabled = false` (the native ordered evaluation agrees), and no matching rule
318
+ * enables it. A whole-plugin `[plugins."<id>"] enabled = false` table is never
319
+ * enough: remote state can enable a locally disabled remote plugin, and a
320
+ * cache copy carries no proof that its source is configuration-controlled. */
321
+ export declare function isInertCodexVendorPluginCopy(entry: NativeSkillEntry, cache: string, rules: unknown, read: (path: string) => string): boolean;
322
+ /** Visible, non-blocking warning for the discovery-witness changes the live Codex
323
+ * prompt hook tolerated as a vendor plugin refresh, or undefined when there were
324
+ * none. One bounded line; paths only, never payloads or file contents. */
325
+ export declare function codexPromptDriftWarning(report: ManagedBridgeReport): string | undefined;
288
326
  /** Run before any prompt context load. Missing ownership or reappearing native
289
327
  * discovery files require repair; verified cache availability is not an override. */
290
- export declare function assertManagedAgentBridge(agent: IntegrationAgent, options?: ManagedBridgeOptions): void;
328
+ export declare function assertManagedAgentBridge(agent: IntegrationAgent, options?: ManagedBridgeOptions): ManagedBridgeReport;
291
329
  export declare function normalizeAgentHookPrompt(agent: IntegrationAgent, event: string, prompt: string): string;
292
330
  export declare function hookContextOutput(event: string, result: {
293
331
  context: string;
@@ -0,0 +1,10 @@
1
+ import type { DiscoveryByteBudget } from "./agent-discovery-bytes.js";
2
+ export declare const CLAUDE_ORPHAN_MARKER_MODE: "claude-plugin-orphaned-at-v1";
3
+ /** True only for `<...>/.claude/plugins/cache/<marketplace>/<plugin>/<version>/.orphaned_at`
4
+ * with exactly one cache segment in the path. Every other path, including a
5
+ * marker at another depth, keeps its ordinary exact witness. */
6
+ export declare function isClaudePluginCacheOrphanMarker(path: string): boolean;
7
+ /** Validate on every capture and check; only presence enters the digest.
8
+ * Claude rewrites the timestamp (and may replace the inode) between checks,
9
+ * so neither is bound, but the file may not change during one read. */
10
+ export declare function hashClaudePluginCacheOrphanMarker(path: string, budget?: DiscoveryByteBudget, changes?: Map<string, string>): string | null;
@@ -32,6 +32,15 @@ export interface CodexPluginSourceInput {
32
32
  sourceSha256: string;
33
33
  }
34
34
  type Read = (path: string) => string;
35
+ /** The remote plugin identifier from the exact native install receipt Codex
36
+ * writes beside a remote plugin's versions, or null when it is absent or invalid. */
37
+ export declare function codexRemoteInstallReceipt(parent: string, read: Read): string | null;
38
+ /** Whether a vendor refresh's `.app.json` connector declaration is valid under
39
+ * the same appControls validation rootControls applies (duplicate keys, the
40
+ * single `apps` object, app names, `id`/`category`/`required` only, types and
41
+ * caps), read as a regular, unlinked file of at most 1 MiB in strict UTF-8.
42
+ * Every failure is false. */
43
+ export declare function codexAppDeclarationValid(path: string): boolean;
35
44
  export interface CodexPluginRootControls {
36
45
  namespace: string;
37
46
  manifestSha256: string;
@@ -0,0 +1,54 @@
1
+ /** OpenAI's own Codex plugin marketplaces, by their exact native names in Codex
2
+ * 0.162.1. Only these hold vendor plugin copies; every other marketplace,
3
+ * including Hasna's and look-alike names, is never treated as vendor content. */
4
+ export declare const CODEX_VENDOR_MARKETPLACES: readonly string[];
5
+ /** Vendor marketplaces whose plugins Codex installs remotely and records with a
6
+ * `.codex-remote-plugin-install.json` receipt beside their versions. */
7
+ export declare const CODEX_REMOTE_VENDOR_MARKETPLACES: readonly string[];
8
+ /** The allowlisted vendor marketplace a canonical path lies in, at or below
9
+ * `<cache>/<marketplace>`, or null for the cache itself, other marketplaces and
10
+ * any non-canonical path. */
11
+ export declare function codexVendorMarketplace(cache: string, path: string): string | null;
12
+ /** A plugin version root a vendor refresh can add without review: its only
13
+ * manifest is a regular `.codex-plugin/plugin.json` (alone in its directory,
14
+ * no duplicate keys) whose keys are all in VENDOR_MANIFEST_KEYS, with the
15
+ * default skills root and, when present, exactly the `./.app.json` connector
16
+ * declaration backed by an `.app.json` that passes the strict connector
17
+ * validation rootControls applies (codexAppDeclarationValid); there is no hooks, MCP, command
18
+ * or agent location and no root, Claude or Cursor manifest; every skill in it
19
+ * is visible to the native inventory. Errors and bounds refuse. */
20
+ export declare function codexVendorPluginRootRefreshSafe(root: string, budget?: {
21
+ remaining: number;
22
+ }): boolean;
23
+ /** True only when every cached version of one vendor plugin
24
+ * (`<cache>/<marketplace>/<plugin>`) passes codexVendorPluginRootRefreshSafe,
25
+ * the plugin directory carries no manifest, hooks, MCP, command or agent
26
+ * location of its own, and no version is a link. Errors and bounds refuse. */
27
+ export declare function codexVendorPluginRefreshSafe(parent: string, budget?: {
28
+ remaining: number;
29
+ }): boolean;
30
+ /** True only when every plugin in a vendor marketplace subtree could have been
31
+ * added by a routine vendor refresh: each plugin passes
32
+ * codexVendorPluginRefreshSafe and the marketplace holds no link, special file
33
+ * or hidden directory. Plugin hooks and other skill roots need their own
34
+ * review, so a membership change that adds them is never a routine vendor
35
+ * refresh. The whole marketplace is checked, not only new versions, so anything
36
+ * unexpected anywhere refuses. Bounded; links are never followed; errors refuse. */
37
+ export declare function codexVendorSubtreeHookFree(cache: string, path: string): boolean;
38
+ /** Candidate earlier versions of a Codex config.toml that differ from `text`
39
+ * only by appended vendor disable rows and/or vendor plugin disables,
40
+ * the entries an operator or Codex adds when a vendor plugin refreshes. Each
41
+ * candidate is checked to parse to exactly `text` without those entries, so a
42
+ * candidate whose digest equals the reviewed witness proves the change is
43
+ * vendor-only. Anything the text model cannot remove exactly yields no
44
+ * candidate, which keeps the witness strict.
45
+ *
46
+ * Bounds: at most 8 vendor plugin tables, 16 trailing rows and 128 candidates,
47
+ * tried smallest edit first (fewest changed tables, then fewest removed rows).
48
+ * `rowsAffectDigest: false` (a typed V2-V5 witness, which ignores disable-only
49
+ * rows) skips row removal; with it, every change to at most two of eight
50
+ * tables fits the 128 candidates. Otherwise appended rows alone always fit.
51
+ * A bound only ever omits candidates, so it can only cause a refusal. */
52
+ export declare function codexVendorConfigCandidates(text: string, cache: string, options?: {
53
+ rowsAffectDigest?: boolean;
54
+ }): string[];
@@ -39,6 +39,8 @@ export interface SkillsConfig {
39
39
  defaultScope?: "global" | "project";
40
40
  format?: "compact" | "json" | "csv";
41
41
  extensionsDir?: string;
42
+ /** Managed hooks, global scope only: "auto" (default) migrates a refused expired session pin; "off" keeps the refusal. */
43
+ sessionPinMigration?: "auto" | "off";
42
44
  }
43
45
  export type ConfigScope = "global" | "project";
44
46
  /**
@@ -97,6 +99,24 @@ export declare function loadConfigReadOnly(): SkillsConfig;
97
99
  * Get the config file path for a given scope
98
100
  */
99
101
  export declare function getConfigPath(scope: ConfigScope): string;
102
+ /**
103
+ * Managed hooks migrate a session pin whose renewal was refused only because
104
+ * its profile moved on, unless the station turns that off with
105
+ * `skills config set sessionPinMigration off --global`. Only the global
106
+ * configuration is read: the prompt hook never reads a project-controlled file
107
+ * for this.
108
+ *
109
+ * This reads the file itself instead of readConfigFile, which returns {} for
110
+ * an unreadable or malformed file. An opt-out must survive a damaged file, so
111
+ * "auto" needs one of two confirmed states. Either lstat finds no entry at the
112
+ * path and its directory exists, or the entry is a regular file (not a
113
+ * symlink), opened without following links, holding a JSON object without the
114
+ * key or with exactly "auto". Everything else keeps the refusal: a dangling or
115
+ * other symlink, a directory or other non-regular entry, a missing or dangling
116
+ * data directory, any read error, a parse error (including a truncated write),
117
+ * a non-object document, a retired key, or a value other than "auto" or "off".
118
+ */
119
+ export declare function sessionPinMigrationEnabled(): boolean;
100
120
  /**
101
121
  * Load merged config: project-local overrides global
102
122
  */
@@ -3,7 +3,9 @@ export declare const HOOK_REFUSAL_REASON_CODES: readonly string[];
3
3
  export declare class HookDiagnosticError extends Error {
4
4
  readonly code: string;
5
5
  readonly stage: "context" | "sync";
6
- constructor(code: string, stage: "context" | "sync");
6
+ /** Fixed marker: a refused renewal whose automatic pin migration ran out of budget. */
7
+ readonly migration?: "timeout";
8
+ constructor(code: string, stage: "context" | "sync", migration?: "timeout");
7
9
  }
8
10
  export declare function hookChildError(stdout: string, stage: "context" | "sync"): HookDiagnosticError;
9
11
  export declare function hookFailureReason(error: unknown, profileId?: string): string;
@@ -0,0 +1,25 @@
1
+ import type { DiscoverySource } from "./agent-discovery.js";
2
+ /** What changed under a reviewed native discovery witness: a bound source
3
+ * file's projected bytes, or a bound directory's membership. */
4
+ export type NativeDiscoveryWitnessKind = "input" | "membership";
5
+ /** A reviewed discovery witness no longer matches the current file system. It is
6
+ * drift of something the review bound, never an invalid, missing or unresolvable
7
+ * binding, which keep their own plain errors. The message is unchanged from the
8
+ * plain error each site threw before, so every caller that does not tolerate
9
+ * these sees exactly the same refusal. */
10
+ export declare class NativeDiscoveryWitnessChange extends Error {
11
+ readonly kind: NativeDiscoveryWitnessKind;
12
+ /** Canonical path the witness bound, or null when the site names none. */
13
+ readonly path: string | null;
14
+ constructor(kind: NativeDiscoveryWitnessKind, path: string | null, message: string);
15
+ }
16
+ /** Live Codex prompt hook only. Decides, per changed witness, whether the change
17
+ * is provably one a Codex vendor plugin refresh makes; accepted changes are
18
+ * collected instead of refusing. Every other change still throws. */
19
+ export interface NativeDiscoveryWitnessTolerance {
20
+ acceptsInput(source: DiscoverySource): boolean;
21
+ acceptsMembership(path: string): boolean;
22
+ readonly changes: NativeDiscoveryWitnessChange[];
23
+ }
24
+ /** Throw the change unless an opted-in tolerance accepted it. */
25
+ export declare function witnessChanged(change: NativeDiscoveryWitnessChange, tolerance: NativeDiscoveryWitnessTolerance | undefined, accepted: boolean): void;
@@ -18,6 +18,11 @@ export interface SkillSessionParentBinding {
18
18
  generation: number;
19
19
  receiptSha256: string;
20
20
  }
21
+ /** The project lock a session's pin was inherited from: its absolute path and the SHA-256 of its exact bytes. */
22
+ export interface SkillSessionProjectLock {
23
+ path: string;
24
+ sha256: string;
25
+ }
21
26
  export interface SkillSessionReceipt extends CachedSelectionProfile {
22
27
  sessionId: string;
23
28
  loaded: string[];
@@ -25,6 +30,12 @@ export interface SkillSessionReceipt extends CachedSelectionProfile {
25
30
  generation?: number;
26
31
  /** Private local metadata binding a child receipt to the exact parent snapshot it inherited. */
27
32
  parent?: SkillSessionParentBinding;
33
+ /**
34
+ * Set when the session was created from a project lock (and inherited by its
35
+ * child sessions). Such a pin follows that intentional lock, so the managed
36
+ * hook never migrates it automatically, whatever the prompt's directory.
37
+ */
38
+ projectLock?: SkillSessionProjectLock;
28
39
  }
29
40
  export interface SkillSessionSnapshot extends SkillSessionParentBinding {
30
41
  path: string;
@@ -60,6 +71,11 @@ export declare function assertFreshCachedProfile(receipt: CachedSelectionProfile
60
71
  }): void;
61
72
  export declare function projectSelectionLockPath(projectDir: string): string;
62
73
  export declare function readProjectSelection(projectDir: string): CachedSelectionProfile | null;
74
+ /** The project lock with the provenance a session created from it records. */
75
+ export declare function readProjectSelectionSnapshot(projectDir: string): {
76
+ receipt: CachedSelectionProfile;
77
+ lock: SkillSessionProjectLock;
78
+ } | null;
63
79
  export declare function sessionReceiptPath(sessionId: string, options?: SelectionCacheOptions): string;
64
80
  export declare function readSkillSession(sessionId: string, options?: SelectionCacheOptions): SkillSessionReceipt | null;
65
81
  export declare function readSkillSessionSnapshotIfExists(sessionId: string, options?: SelectionCacheOptions): SkillSessionSnapshot | null;
@@ -77,6 +93,12 @@ export declare function inspectSessionWriteLock(sessionId: string, options?: Sel
77
93
  hostBound: boolean;
78
94
  };
79
95
  export declare function recoverSessionWriteLock(sessionId: string, reviewDigest: string, options?: SelectionCacheOptions): string;
96
+ /**
97
+ * Hold this session's receipt write lock for state that must change only with
98
+ * its pin (the managed-hook migration notice). Another holder refuses with
99
+ * SESSION_WRITE_BUSY; nothing waits.
100
+ */
101
+ export declare function withSkillSessionWriteLock<T>(sessionId: string, options: SelectionCacheOptions, action: (assertOwned: () => void) => T): T;
80
102
  export declare function nextSkillSessionGeneration(generation: number): number;
81
103
  /** Receipt creation and updates are exact-snapshot CAS operations. Child creation also fences its parent. */
82
104
  export declare function writeSkillSession(receipt: SkillSessionReceipt, expected: SkillSessionWritePrecondition, options?: SelectionCacheOptions): void;
@@ -2,7 +2,7 @@
2
2
  import { type ProfileClient } from "./profile-client.js";
3
3
  import type { ResolvedSkillProfile, ResolvedSkillSelection } from "../types/skill-selection.js";
4
4
  import type { SkillBundleEntry } from "./skill-bundle.js";
5
- import { type CachedSelectionProfile, type SelectionCacheOptions, type SkillSessionParentBinding, type SkillSessionReceipt } from "./selection-cache.js";
5
+ import { type CachedSelectionProfile, type SelectionCacheOptions, type SkillSessionParentBinding, type SkillSessionProjectLock, type SkillSessionReceipt } from "./selection-cache.js";
6
6
  export interface SelectionResolverOptions extends SelectionCacheOptions {
7
7
  client?: ProfileClient;
8
8
  cached?: boolean;
@@ -36,6 +36,8 @@ export interface ResolvedSelectionContext {
36
36
  parentSnapshot?: SkillSessionParentBinding;
37
37
  inheritedLoaded?: string[];
38
38
  client?: ProfileClient;
39
+ /** Set when a new session's pin comes from the project lock; the session receipt records it. */
40
+ projectLock?: SkillSessionProjectLock;
39
41
  }
40
42
  export declare function resolveSelectionContext(profileId: string, options?: SelectionResolverOptions): Promise<ResolvedSelectionContext>;
41
43
  export declare function readSelectedEntries(selection: ResolvedSkillSelection, context: ResolvedSelectionContext, options?: SelectionResolverOptions): Promise<SkillBundleEntry[]>;
@@ -1,5 +1,5 @@
1
1
  import { type ProfileClient } from "./profile-client.js";
2
- import { type SelectionCacheOptions } from "./selection-cache.js";
2
+ import { SkillSelectionError, type SelectionCacheOptions } from "./selection-cache.js";
3
3
  export interface SessionReconciliationInput {
4
4
  sessionId: string;
5
5
  fromProfile: string;
@@ -58,8 +58,21 @@ export declare const SESSION_RENEWAL_TIMEOUT_MS = 4000;
58
58
  * It binds the exact receipt bytes, profile, authority, workspace and the
59
59
  * locally synced profile revision, and lapses after a fixed window that the
60
60
  * file cannot extend. Anything unexpected is ignored and the authority is asked.
61
+ *
62
+ * Two kinds exist. A renewal refusal says only that this pin cannot be renewed
63
+ * as it is, so it is repeated only while migration is off. A refusal of the
64
+ * managed hook's migration itself (a project lock, a rotated epoch, a planner
65
+ * refusal or anything else that is not transient) also implies the renewal
66
+ * refusal, so it is repeated in either mode. Timeouts, an unavailable
67
+ * authority, write-lock contention and other transient failures are never
68
+ * remembered.
61
69
  */
62
70
  export declare const SESSION_RENEWAL_REFUSAL_TTL_MS: number;
71
+ /**
72
+ * Return the pending migration notice for this session once its pin holds the
73
+ * migrated profile, then mark it delivered. Anything unexpected returns null.
74
+ */
75
+ export declare function takeSessionPinMigrationNotice(sessionId: string, options?: SelectionCacheOptions): string | null;
63
76
  /**
64
77
  * Renew authorization for the complete immutable pin; never manufacture a
65
78
  * profile combining historical loaded versions with current unloaded versions.
@@ -67,6 +80,49 @@ export declare const SESSION_RENEWAL_REFUSAL_TTL_MS: number;
67
80
  * versions additionally require the authenticated exact-version bundle read.
68
81
  */
69
82
  export declare function reconcileSkillSessionIfSafe(sessionId: string, profileId: string, options?: SessionReconciliationOptions): Promise<boolean>;
83
+ /** Metadata only: what an automatic migration retired, never payloads. */
84
+ export interface SessionPinMigration {
85
+ profileId: string;
86
+ fromRevision: string;
87
+ toRevision: string;
88
+ retainedLoadedCount: number;
89
+ retiredLoadedCount: number;
90
+ retired: SessionSelectionDelta[];
91
+ archivePath: string;
92
+ receiptPath: string;
93
+ }
94
+ export type SessionPinRenewalOutcome = {
95
+ outcome: "unchanged";
96
+ } | {
97
+ outcome: "renewed";
98
+ } | {
99
+ outcome: "migrated";
100
+ migration: SessionPinMigration;
101
+ };
102
+ export interface SessionPinRenewalOptions extends SessionReconciliationOptions {
103
+ /**
104
+ * Managed hooks only: when renewal is refused because the same profile moved
105
+ * on, apply the explicit reconciliation plan to the authenticated current
106
+ * revision instead. Every other failure keeps its existing refusal.
107
+ */
108
+ migrate?: boolean;
109
+ /**
110
+ * Required for `migrate`: the prompt's directory (the hook input's cwd).
111
+ * Without it, or while it or an ancestor holds a project lock, migration
112
+ * does not run.
113
+ */
114
+ projectDir?: string;
115
+ }
116
+ /**
117
+ * A refused renewal whose automatic migration could not finish within the
118
+ * renewal deadline. It keeps the refusal code; the fixed `migration` marker
119
+ * lets the hook say why without carrying free text across its boundary.
120
+ */
121
+ export declare class SessionPinMigrationTimeout extends SkillSelectionError {
122
+ readonly migration: "timeout";
123
+ constructor();
124
+ }
125
+ export declare function renewSkillSessionPin(sessionId: string, profileId: string, options?: SessionPinRenewalOptions): Promise<SessionPinRenewalOutcome>;
70
126
  /** Explicit review for changed loaded selections or intentional profile migration. */
71
127
  export declare function reconcileSkillSession(input: SessionReconciliationInput, options?: SessionReconciliationOptions): Promise<{
72
128
  applied: false;