@hasna/skills 0.10.64 → 0.10.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -334,6 +334,12 @@ Sumi uses its native V2 plugin API, independently of OpenCode. Its managed
334
334
  `plugins/skills-cli.js` plugin validates prompt admission and adds selected
335
335
  instructions to transient request context. Real native session IDs and parent
336
336
  IDs bind exact inherited pins; native agent definitions are not Hasna identities.
337
+ Prompt matching uses the native prompt-admission event. Request context restores
338
+ verified loaded Skills and never treats model history, attachments or compaction
339
+ checkpoints as a new owner prompt. The complete native model context is unchanged;
340
+ the hook's input limits and native/integrity refusals remain strict. Adapter
341
+ refusals retain only fixed reason codes, never child diagnostic prose. Both older
342
+ managed plugin forms remain recognized for a reviewed hook-install upgrade.
337
343
  Native payload tool calls and prompt attachments are refused except for the
338
344
  `skills-cli` bridge. The supported configuration resolver honors `SUMI_CONFIG_DIR`,
339
345
  `XDG_CONFIG_HOME` and `SUMI_HOME` without performing native home adoption.
package/bin/index.js CHANGED
@@ -36888,7 +36888,7 @@ var package_default;
36888
36888
  var init_package = __esm(() => {
36889
36889
  package_default = {
36890
36890
  name: "@hasna/skills",
36891
- version: "0.10.64",
36891
+ version: "0.10.65",
36892
36892
  description: "Skills library for AI coding agents",
36893
36893
  type: "module",
36894
36894
  bin: {
@@ -54750,6 +54750,130 @@ var init_agent_codex = __esm(() => {
54750
54750
  CODEX_SKILL_CONFIG_SECTIONS = /^\[\[skills\.config\]\][^\n]*(?:\n(?!\s*\[)[^\n]*)*/gm;
54751
54751
  });
54752
54752
 
54753
+ // src/lib/hook-diagnostics.ts
54754
+ function hookChildError(stdout, stage) {
54755
+ let code;
54756
+ if (stdout.length <= 16 * 1024) {
54757
+ try {
54758
+ code = JSON.parse(stdout)?.error?.code;
54759
+ } catch {}
54760
+ }
54761
+ return new HookDiagnosticError(typeof code === "string" && CONTEXT_ERROR_CODES.has(code) ? code : "SKILLS_CONTEXT_FAILED", stage);
54762
+ }
54763
+ function hookFailureReason(error, profileId) {
54764
+ const owned = error instanceof HookDiagnosticError;
54765
+ const code = owned && (CONTEXT_ERROR_CODES.has(error.code) || ["SKILLS_HOOK_TIMEOUT", "SKILLS_HOOK_INVALID_RESPONSE"].includes(error.code)) ? error.code : "SKILLS_HOOK_FAILED";
54766
+ const profile = typeof profileId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) && !profileId.includes("..") ? profileId : undefined;
54767
+ const identity6 = [profile ? `profile=${profile}` : undefined, owned ? `stage=${error.stage}` : undefined].filter(Boolean).join(", ");
54768
+ const prefix = `Skills context is unavailable [${code}]${identity6 ? ` (${identity6})` : ""}.`;
54769
+ if (code === "SKILLS_STORAGE_FULL") {
54770
+ return `${prefix} The filesystem is full or its quota is exhausted. Free space through your approved recoverable cleanup process, then retry the same session. Do not delete session receipts or change credentials to bypass this failure.`;
54771
+ }
54772
+ if (code === "PROFILE_LOCK_MISMATCH") {
54773
+ return `${prefix} This session or project is pinned to a different profile. Inspect the session with skills sessions show <session-id> --json; review skills sessions reconcile for an intentional session change, or explicit project sync for a project change. Sync alone does not change that pin.`;
54774
+ }
54775
+ if (code === "SESSION_RECONCILIATION_REQUIRED") {
54776
+ return `${prefix} A loaded skill changed or was removed. Inspect this session with skills sessions show <session-id> --json, then review skills sessions reconcile for an intentional change. Sync alone does not change this pin. A refusal is re-checked with the authority within five minutes, or sooner after the receipt or the synced profile revision changes.`;
54777
+ }
54778
+ if (code === "SESSION_RENEWAL_TIMEOUT") {
54779
+ return `${prefix} Renewing this session's expired authorization did not finish within the hook's renewal window; no request was refused. Sync and diagnosis do not change this window. The existing pin is unchanged and the next prompt retries the renewal. If it keeps recurring, inspect the session with skills sessions show <session-id> --json and review skills sessions reconcile.`;
54780
+ }
54781
+ if (code.startsWith("SESSION_")) {
54782
+ return `${prefix} Inspect the session with skills sessions show <session-id> --json and review its parent or concurrent writer before retrying. Do not remove or rewrite its receipt to bypass this refusal.`;
54783
+ }
54784
+ if (["PROFILE_IDENTITY_MISMATCH", "BUNDLE_DIGEST_MISMATCH", "BUNDLE_IDENTITY_MISMATCH", "INVALID_RECEIPT", "INVALID_CACHE_FILE", "UNSAFE_CACHE_PATH"].includes(code)) {
54785
+ return `${prefix} Verify the selected authority and receipt integrity before repairing the profile; do not bypass verification or substitute another cache.`;
54786
+ }
54787
+ const selected = profile ?? "<id>";
54788
+ return `${prefix} Run skills sync --selection-profile ${selected} --json, then diagnose with skills context --stdin --json --cached --selection-profile ${selected} using the same hook session and working-directory fields. Do not include credentials in diagnostic input.`;
54789
+ }
54790
+ function isOptionalHookContextFailure(error) {
54791
+ return error instanceof HookDiagnosticError && OPTIONAL_CONTEXT_FAILURES.has(error.code);
54792
+ }
54793
+ function hookUnavailableContext(error, profileId) {
54794
+ return `${hookFailureReason(error, profileId)} No Skills instructions were delivered by this hook. Ordinary work may continue. Do not perform actions that depend on unavailable Skills instructions, including previously loaded instructions. Explicit skills load, context and run still require verification. Do not substitute native, unmanaged or stale payloads.`;
54795
+ }
54796
+ var CONTEXT_ERROR_CODES, HOOK_REFUSAL_REASON_CODES, HookDiagnosticError, OPTIONAL_CONTEXT_FAILURES;
54797
+ var init_hook_diagnostics = __esm(() => {
54798
+ CONTEXT_ERROR_CODES = new Set([
54799
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
54800
+ "SKILLS_API_UNAVAILABLE",
54801
+ "SKILLS_API_UNAUTHORIZED",
54802
+ "SKILLS_API_FORBIDDEN",
54803
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
54804
+ "SESSION_WRITE_BUSY",
54805
+ "SKILLS_CONTEXT_FAILED",
54806
+ "SKILLS_STORAGE_FULL",
54807
+ "PROFILE_LOCK_MISMATCH",
54808
+ "PROFILE_IDENTITY_MISMATCH",
54809
+ "PROFILE_APPLIED_REPORT_FAILED",
54810
+ "CACHED_AUTHORITY_REQUIRED",
54811
+ "CACHED_PROFILE_EXPIRED",
54812
+ "CACHED_PROFILE_MISSING",
54813
+ "CACHED_BUNDLE_MISSING",
54814
+ "INVALID_CONTEXT_INPUT",
54815
+ "CONTEXT_INPUT_TOO_LARGE",
54816
+ "INVALID_CONTEXT_BUDGET",
54817
+ "INVALID_SELECTION",
54818
+ "INVALID_SELECTION_ALIASES",
54819
+ "INVALID_RECEIPT",
54820
+ "INVALID_CACHE_FILE",
54821
+ "UNSAFE_CACHE_PATH",
54822
+ "RECEIPT_TOO_LARGE",
54823
+ "BUNDLE_DIGEST_MISMATCH",
54824
+ "BUNDLE_IDENTITY_MISMATCH",
54825
+ "BUNDLE_UNAVAILABLE",
54826
+ "BUNDLE_TOO_LARGE",
54827
+ "SKILL_NOT_SELECTED",
54828
+ "INVALID_SESSION",
54829
+ "SESSION_PARENT_NOT_FOUND",
54830
+ "SESSION_PARENT_MISMATCH",
54831
+ "SESSION_NOT_FOUND",
54832
+ "SESSION_GENERATION_CHANGED",
54833
+ "SESSION_GENERATION_EXHAUSTED",
54834
+ "SESSION_PARENT_CHANGED",
54835
+ "SESSION_RECEIPT_CHANGED",
54836
+ "SESSION_RECONCILIATION_REQUIRED",
54837
+ "SESSION_RENEWAL_TIMEOUT",
54838
+ "SESSION_CONTEXT_CONFLICT",
54839
+ "SESSION_RECONCILIATION_INCOMPLETE",
54840
+ "SESSION_WRITE_LOCKED",
54841
+ "SESSION_WRITE_LOCK_CHANGED",
54842
+ "SESSION_LOCK_RECOVERY_INCOMPLETE"
54843
+ ]);
54844
+ HOOK_REFUSAL_REASON_CODES = Object.freeze([
54845
+ ...CONTEXT_ERROR_CODES,
54846
+ "SKILLS_HOOK_FAILED",
54847
+ "SKILLS_HOOK_TIMEOUT",
54848
+ "SKILLS_HOOK_INVALID_RESPONSE"
54849
+ ]);
54850
+ HookDiagnosticError = class HookDiagnosticError extends Error {
54851
+ code;
54852
+ stage;
54853
+ constructor(code, stage) {
54854
+ super("Skills hook operation failed");
54855
+ this.code = code;
54856
+ this.stage = stage;
54857
+ }
54858
+ };
54859
+ OPTIONAL_CONTEXT_FAILURES = new Set([
54860
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
54861
+ "SKILLS_API_UNAVAILABLE",
54862
+ "SKILLS_API_UNAUTHORIZED",
54863
+ "SKILLS_API_FORBIDDEN",
54864
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
54865
+ "CACHED_PROFILE_EXPIRED",
54866
+ "CACHED_PROFILE_MISSING",
54867
+ "CACHED_BUNDLE_MISSING",
54868
+ "SESSION_RECONCILIATION_REQUIRED",
54869
+ "SESSION_RENEWAL_TIMEOUT",
54870
+ "SESSION_WRITE_BUSY",
54871
+ "SESSION_PARENT_CHANGED",
54872
+ "SESSION_CONTEXT_CONFLICT",
54873
+ "SKILLS_HOOK_TIMEOUT"
54874
+ ]);
54875
+ });
54876
+
54753
54877
  // src/lib/agent-sumi.ts
54754
54878
  import { spawnSync as spawnSync3 } from "child_process";
54755
54879
  import { accessSync as accessSync2, constants as constants12, existsSync as existsSync12, lstatSync as lstatSync19, realpathSync as realpathSync9, statSync as statSync9 } from "fs";
@@ -54883,24 +55007,33 @@ function sumiConfigPath(home, cwd2 = process.cwd(), env3 = process.env) {
54883
55007
  return join28(sumiConfigDirectory(home, env3, cwd2), "sumi.json");
54884
55008
  }
54885
55009
  function renderSumiPlugin(command, profile) {
54886
- return renderSumiPluginVersion(command, profile, false);
55010
+ return renderSumiPluginVersion(command, profile, 3);
54887
55011
  }
54888
55012
  function isManagedSumiPlugin(text2, command, profile) {
54889
- return text2 === renderSumiPlugin(command, profile) || text2 === renderSumiPluginVersion(command, profile, true);
55013
+ return text2 === renderSumiPlugin(command, profile) || text2 === renderSumiPluginVersion(command, profile, 2) || text2 === renderSumiPluginVersion(command, profile, 1);
54890
55014
  }
54891
- function renderSumiPluginVersion(command, profile, legacy) {
55015
+ function renderSumiPluginVersion(command, profile, version) {
55016
+ const legacy = version === 1;
54892
55017
  const failure = (message) => legacy ? `new Error(${JSON.stringify(message)})` : "refusal()";
54893
55018
  return `// Managed by @hasna/skills. Regenerate with skills hook install --agent sumi.
54894
55019
  import { spawn } from "node:child_process";
54895
55020
  const command = ${JSON.stringify(command)};
54896
55021
  const profile = ${JSON.stringify(profile)};
54897
- ${legacy ? "" : `function refusal() {
54898
- const error = new Error("Skills verification blocked this request. Review the Sumi Skills hook configuration, then retry.");
55022
+ ${legacy ? "" : `function refusal(${version === 3 ? "reasonCode" : ""}) {
55023
+ const error = new Error("Skills verification blocked this request. Review the Sumi Skills hook configuration, then retry."${version === 3 ? ' + (reasonCode ? " [" + reasonCode + "]" : "")' : ""});
54899
55024
  error.name = "SkillsHookRefusal";
54900
- error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
55025
+ error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED"${version === 3 ? ", ...(reasonCode ? { reasonCode } : {})" : ""} };
54901
55026
  return error;
54902
55027
  }
54903
- `}function context(input) {
55028
+ `}${version === 3 ? `const reasonCodes = new Set(${JSON.stringify(HOOK_REFUSAL_REASON_CODES)});
55029
+ function refusedResult(result) {
55030
+ const reason = result.reason ?? result.stopReason ?? result.systemMessage;
55031
+ if (typeof reason !== "string" || reason.length > 16384) return refusal();
55032
+ if (reason.startsWith("NATIVE_SKILL_DRIFT:")) return refusal("NATIVE_SKILL_DRIFT");
55033
+ const code = /^Skills context is unavailable \\[([A-Z_]+)\\]/.exec(reason)?.[1];
55034
+ return refusal(reasonCodes.has(code) ? code : undefined);
55035
+ }
55036
+ ` : ""}function context(input) {
54904
55037
  return new Promise((resolve, reject) => {
54905
55038
  const child = spawn(command, ["hook", "user-prompt", "--agent", "sumi", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
54906
55039
  const chunks = []; let bytes = 0, failure;
@@ -54915,12 +55048,12 @@ ${legacy ? "" : `function refusal() {
54915
55048
  try {
54916
55049
  const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
54917
55050
  if (!result || typeof result !== "object" || Array.isArray(result)) throw ${failure("Invalid Skills hook response")};
54918
- if (result.decision === "block" || result.continue === false) throw ${failure("Skills prompt hook refused")};
55051
+ if (result.decision === "block" || result.continue === false) throw ${version === 3 ? "refusedResult(result)" : failure("Skills prompt hook refused")};
54919
55052
  if (Object.keys(result).length && (!result.hookSpecificOutput || result.hookSpecificOutput.hookEventName !== input.hook_event_name)) throw ${failure("Invalid Skills hook response")};
54920
55053
  const text = result.hookSpecificOutput?.additionalContext ?? "";
54921
55054
  if (typeof text !== "string") throw ${failure("Invalid Skills prompt context")};
54922
55055
  resolve(text);
54923
- } catch (error) { reject(${legacy ? "error" : "refusal()"}); }
55056
+ } catch (error) { reject(${legacy ? "error" : version === 3 ? 'error instanceof Error && error.name === "SkillsHookRefusal" ? error : refusal()' : "refusal()"}); }
54924
55057
  });
54925
55058
  child.stdin.end(JSON.stringify(input));
54926
55059
  });
@@ -54953,9 +55086,13 @@ export default {
54953
55086
  sessions.add(event.sessionID);
54954
55087
  }));
54955
55088
  registrations.push(await ctx.session.hook("context", async event => {
54956
- const last = event.messages.findLast(message => message.role === "user");
55089
+ ${version === 3 ? ` // Model messages also contain checkpoints, attachments and synthetic
55090
+ // user-role history, with no owner-prompt provenance. Prompt admission
55091
+ // already matched and persisted Skills; restore that verified state only.
55092
+ const prompt = "";
55093
+ ` : ` const last = event.messages.findLast(message => message.role === "user");
54957
55094
  const prompt = (last?.content ?? []).filter(part => part.type === "text").map(part => part.text).join("\\n");
54958
- // Recheck on every primary request, including resumed and synthetic
55095
+ `} // Recheck on every primary request, including resumed and synthetic
54959
55096
  // sessions. The CLI owns optional failures versus explicit refusals.
54960
55097
  const text = await context(await input(event.sessionID, prompt, !sessions.has(event.sessionID)));
54961
55098
  if (text) event.system.push({ type: "text", text });
@@ -54968,6 +55105,7 @@ export default {
54968
55105
  }
54969
55106
  var SUMI_PATH_SELECTORS, SUMI_PATH_KEYS, SUMI_PATH_OUTPUT_LIMIT, SumiPathResolverError, SUMI_SKILL_PERMISSIONS;
54970
55107
  var init_agent_sumi = __esm(() => {
55108
+ init_hook_diagnostics();
54971
55109
  SUMI_PATH_SELECTORS = ["SUMI_HOME", "SUMI_CONFIG_DIR", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME"];
54972
55110
  SUMI_PATH_KEYS = ["data", "cache", "config", "state"];
54973
55111
  SUMI_PATH_OUTPUT_LIMIT = 128 * 1024;
@@ -118034,124 +118172,6 @@ var init_codex_native_trust = __esm(() => {
118034
118172
  HEX_DIGEST2 = /^[0-9a-f]{64}$/;
118035
118173
  });
118036
118174
 
118037
- // src/lib/hook-diagnostics.ts
118038
- function hookChildError(stdout, stage) {
118039
- let code2;
118040
- if (stdout.length <= 16 * 1024) {
118041
- try {
118042
- code2 = JSON.parse(stdout)?.error?.code;
118043
- } catch {}
118044
- }
118045
- return new HookDiagnosticError(typeof code2 === "string" && CONTEXT_ERROR_CODES.has(code2) ? code2 : "SKILLS_CONTEXT_FAILED", stage);
118046
- }
118047
- function hookFailureReason(error61, profileId) {
118048
- const owned = error61 instanceof HookDiagnosticError;
118049
- const code2 = owned && (CONTEXT_ERROR_CODES.has(error61.code) || ["SKILLS_HOOK_TIMEOUT", "SKILLS_HOOK_INVALID_RESPONSE"].includes(error61.code)) ? error61.code : "SKILLS_HOOK_FAILED";
118050
- const profile = typeof profileId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) && !profileId.includes("..") ? profileId : undefined;
118051
- const identity6 = [profile ? `profile=${profile}` : undefined, owned ? `stage=${error61.stage}` : undefined].filter(Boolean).join(", ");
118052
- const prefix = `Skills context is unavailable [${code2}]${identity6 ? ` (${identity6})` : ""}.`;
118053
- if (code2 === "SKILLS_STORAGE_FULL") {
118054
- return `${prefix} The filesystem is full or its quota is exhausted. Free space through your approved recoverable cleanup process, then retry the same session. Do not delete session receipts or change credentials to bypass this failure.`;
118055
- }
118056
- if (code2 === "PROFILE_LOCK_MISMATCH") {
118057
- return `${prefix} This session or project is pinned to a different profile. Inspect the session with skills sessions show <session-id> --json; review skills sessions reconcile for an intentional session change, or explicit project sync for a project change. Sync alone does not change that pin.`;
118058
- }
118059
- if (code2 === "SESSION_RECONCILIATION_REQUIRED") {
118060
- return `${prefix} A loaded skill changed or was removed. Inspect this session with skills sessions show <session-id> --json, then review skills sessions reconcile for an intentional change. Sync alone does not change this pin. A refusal is re-checked with the authority within five minutes, or sooner after the receipt or the synced profile revision changes.`;
118061
- }
118062
- if (code2 === "SESSION_RENEWAL_TIMEOUT") {
118063
- return `${prefix} Renewing this session's expired authorization did not finish within the hook's renewal window; no request was refused. Sync and diagnosis do not change this window. The existing pin is unchanged and the next prompt retries the renewal. If it keeps recurring, inspect the session with skills sessions show <session-id> --json and review skills sessions reconcile.`;
118064
- }
118065
- if (code2.startsWith("SESSION_")) {
118066
- return `${prefix} Inspect the session with skills sessions show <session-id> --json and review its parent or concurrent writer before retrying. Do not remove or rewrite its receipt to bypass this refusal.`;
118067
- }
118068
- if (["PROFILE_IDENTITY_MISMATCH", "BUNDLE_DIGEST_MISMATCH", "BUNDLE_IDENTITY_MISMATCH", "INVALID_RECEIPT", "INVALID_CACHE_FILE", "UNSAFE_CACHE_PATH"].includes(code2)) {
118069
- return `${prefix} Verify the selected authority and receipt integrity before repairing the profile; do not bypass verification or substitute another cache.`;
118070
- }
118071
- const selected = profile ?? "<id>";
118072
- return `${prefix} Run skills sync --selection-profile ${selected} --json, then diagnose with skills context --stdin --json --cached --selection-profile ${selected} using the same hook session and working-directory fields. Do not include credentials in diagnostic input.`;
118073
- }
118074
- function isOptionalHookContextFailure(error61) {
118075
- return error61 instanceof HookDiagnosticError && OPTIONAL_CONTEXT_FAILURES.has(error61.code);
118076
- }
118077
- function hookUnavailableContext(error61, profileId) {
118078
- return `${hookFailureReason(error61, profileId)} No Skills instructions were delivered by this hook. Ordinary work may continue. Do not perform actions that depend on unavailable Skills instructions, including previously loaded instructions. Explicit skills load, context and run still require verification. Do not substitute native, unmanaged or stale payloads.`;
118079
- }
118080
- var CONTEXT_ERROR_CODES, HookDiagnosticError, OPTIONAL_CONTEXT_FAILURES;
118081
- var init_hook_diagnostics = __esm(() => {
118082
- CONTEXT_ERROR_CODES = new Set([
118083
- "SKILLS_API_CREDENTIAL_UNAVAILABLE",
118084
- "SKILLS_API_UNAVAILABLE",
118085
- "SKILLS_API_UNAUTHORIZED",
118086
- "SKILLS_API_FORBIDDEN",
118087
- "SKILLS_API_RESOURCE_UNAVAILABLE",
118088
- "SESSION_WRITE_BUSY",
118089
- "SKILLS_CONTEXT_FAILED",
118090
- "SKILLS_STORAGE_FULL",
118091
- "PROFILE_LOCK_MISMATCH",
118092
- "PROFILE_IDENTITY_MISMATCH",
118093
- "PROFILE_APPLIED_REPORT_FAILED",
118094
- "CACHED_AUTHORITY_REQUIRED",
118095
- "CACHED_PROFILE_EXPIRED",
118096
- "CACHED_PROFILE_MISSING",
118097
- "CACHED_BUNDLE_MISSING",
118098
- "INVALID_CONTEXT_INPUT",
118099
- "CONTEXT_INPUT_TOO_LARGE",
118100
- "INVALID_CONTEXT_BUDGET",
118101
- "INVALID_SELECTION",
118102
- "INVALID_SELECTION_ALIASES",
118103
- "INVALID_RECEIPT",
118104
- "INVALID_CACHE_FILE",
118105
- "UNSAFE_CACHE_PATH",
118106
- "RECEIPT_TOO_LARGE",
118107
- "BUNDLE_DIGEST_MISMATCH",
118108
- "BUNDLE_IDENTITY_MISMATCH",
118109
- "BUNDLE_UNAVAILABLE",
118110
- "BUNDLE_TOO_LARGE",
118111
- "SKILL_NOT_SELECTED",
118112
- "INVALID_SESSION",
118113
- "SESSION_PARENT_NOT_FOUND",
118114
- "SESSION_PARENT_MISMATCH",
118115
- "SESSION_NOT_FOUND",
118116
- "SESSION_GENERATION_CHANGED",
118117
- "SESSION_GENERATION_EXHAUSTED",
118118
- "SESSION_PARENT_CHANGED",
118119
- "SESSION_RECEIPT_CHANGED",
118120
- "SESSION_RECONCILIATION_REQUIRED",
118121
- "SESSION_RENEWAL_TIMEOUT",
118122
- "SESSION_CONTEXT_CONFLICT",
118123
- "SESSION_RECONCILIATION_INCOMPLETE",
118124
- "SESSION_WRITE_LOCKED",
118125
- "SESSION_WRITE_LOCK_CHANGED",
118126
- "SESSION_LOCK_RECOVERY_INCOMPLETE"
118127
- ]);
118128
- HookDiagnosticError = class HookDiagnosticError extends Error {
118129
- code;
118130
- stage;
118131
- constructor(code2, stage) {
118132
- super("Skills hook operation failed");
118133
- this.code = code2;
118134
- this.stage = stage;
118135
- }
118136
- };
118137
- OPTIONAL_CONTEXT_FAILURES = new Set([
118138
- "SKILLS_API_CREDENTIAL_UNAVAILABLE",
118139
- "SKILLS_API_UNAVAILABLE",
118140
- "SKILLS_API_UNAUTHORIZED",
118141
- "SKILLS_API_FORBIDDEN",
118142
- "SKILLS_API_RESOURCE_UNAVAILABLE",
118143
- "CACHED_PROFILE_EXPIRED",
118144
- "CACHED_PROFILE_MISSING",
118145
- "CACHED_BUNDLE_MISSING",
118146
- "SESSION_RECONCILIATION_REQUIRED",
118147
- "SESSION_RENEWAL_TIMEOUT",
118148
- "SESSION_WRITE_BUSY",
118149
- "SESSION_PARENT_CHANGED",
118150
- "SESSION_CONTEXT_CONFLICT",
118151
- "SKILLS_HOOK_TIMEOUT"
118152
- ]);
118153
- });
118154
-
118155
118175
  // src/lib/claude-prospective-review.ts
118156
118176
  import { createHash as createHash43 } from "crypto";
118157
118177
  import { lstatSync as lstatSync41, readFileSync as readFileSync45, readdirSync as readdirSync25, realpathSync as realpathSync22 } from "fs";
package/bin/mcp.js CHANGED
@@ -23,7 +23,7 @@ var package_default;
23
23
  var init_package = __esm(() => {
24
24
  package_default = {
25
25
  name: "@hasna/skills",
26
- version: "0.10.64",
26
+ version: "0.10.65",
27
27
  description: "Skills library for AI coding agents",
28
28
  type: "module",
29
29
  bin: {
@@ -34337,6 +34337,79 @@ var init_agent_bridge = __esm(() => {
34337
34337
  // src/lib/agent-codex.ts
34338
34338
  var init_agent_codex = () => {};
34339
34339
 
34340
+ // src/lib/hook-diagnostics.ts
34341
+ var CONTEXT_ERROR_CODES, HOOK_REFUSAL_REASON_CODES, OPTIONAL_CONTEXT_FAILURES;
34342
+ var init_hook_diagnostics = __esm(() => {
34343
+ CONTEXT_ERROR_CODES = new Set([
34344
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
34345
+ "SKILLS_API_UNAVAILABLE",
34346
+ "SKILLS_API_UNAUTHORIZED",
34347
+ "SKILLS_API_FORBIDDEN",
34348
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
34349
+ "SESSION_WRITE_BUSY",
34350
+ "SKILLS_CONTEXT_FAILED",
34351
+ "SKILLS_STORAGE_FULL",
34352
+ "PROFILE_LOCK_MISMATCH",
34353
+ "PROFILE_IDENTITY_MISMATCH",
34354
+ "PROFILE_APPLIED_REPORT_FAILED",
34355
+ "CACHED_AUTHORITY_REQUIRED",
34356
+ "CACHED_PROFILE_EXPIRED",
34357
+ "CACHED_PROFILE_MISSING",
34358
+ "CACHED_BUNDLE_MISSING",
34359
+ "INVALID_CONTEXT_INPUT",
34360
+ "CONTEXT_INPUT_TOO_LARGE",
34361
+ "INVALID_CONTEXT_BUDGET",
34362
+ "INVALID_SELECTION",
34363
+ "INVALID_SELECTION_ALIASES",
34364
+ "INVALID_RECEIPT",
34365
+ "INVALID_CACHE_FILE",
34366
+ "UNSAFE_CACHE_PATH",
34367
+ "RECEIPT_TOO_LARGE",
34368
+ "BUNDLE_DIGEST_MISMATCH",
34369
+ "BUNDLE_IDENTITY_MISMATCH",
34370
+ "BUNDLE_UNAVAILABLE",
34371
+ "BUNDLE_TOO_LARGE",
34372
+ "SKILL_NOT_SELECTED",
34373
+ "INVALID_SESSION",
34374
+ "SESSION_PARENT_NOT_FOUND",
34375
+ "SESSION_PARENT_MISMATCH",
34376
+ "SESSION_NOT_FOUND",
34377
+ "SESSION_GENERATION_CHANGED",
34378
+ "SESSION_GENERATION_EXHAUSTED",
34379
+ "SESSION_PARENT_CHANGED",
34380
+ "SESSION_RECEIPT_CHANGED",
34381
+ "SESSION_RECONCILIATION_REQUIRED",
34382
+ "SESSION_RENEWAL_TIMEOUT",
34383
+ "SESSION_CONTEXT_CONFLICT",
34384
+ "SESSION_RECONCILIATION_INCOMPLETE",
34385
+ "SESSION_WRITE_LOCKED",
34386
+ "SESSION_WRITE_LOCK_CHANGED",
34387
+ "SESSION_LOCK_RECOVERY_INCOMPLETE"
34388
+ ]);
34389
+ HOOK_REFUSAL_REASON_CODES = Object.freeze([
34390
+ ...CONTEXT_ERROR_CODES,
34391
+ "SKILLS_HOOK_FAILED",
34392
+ "SKILLS_HOOK_TIMEOUT",
34393
+ "SKILLS_HOOK_INVALID_RESPONSE"
34394
+ ]);
34395
+ OPTIONAL_CONTEXT_FAILURES = new Set([
34396
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
34397
+ "SKILLS_API_UNAVAILABLE",
34398
+ "SKILLS_API_UNAUTHORIZED",
34399
+ "SKILLS_API_FORBIDDEN",
34400
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
34401
+ "CACHED_PROFILE_EXPIRED",
34402
+ "CACHED_PROFILE_MISSING",
34403
+ "CACHED_BUNDLE_MISSING",
34404
+ "SESSION_RECONCILIATION_REQUIRED",
34405
+ "SESSION_RENEWAL_TIMEOUT",
34406
+ "SESSION_WRITE_BUSY",
34407
+ "SESSION_PARENT_CHANGED",
34408
+ "SESSION_CONTEXT_CONFLICT",
34409
+ "SKILLS_HOOK_TIMEOUT"
34410
+ ]);
34411
+ });
34412
+
34340
34413
  // src/lib/agent-sumi.ts
34341
34414
  import { spawnSync as spawnSync4 } from "child_process";
34342
34415
  import { accessSync, constants as constants9, existsSync as existsSync8, lstatSync as lstatSync15, realpathSync as realpathSync7, statSync as statSync7 } from "fs";
@@ -34464,6 +34537,7 @@ function sumiConfigPath(home, cwd = process.cwd(), env = process.env) {
34464
34537
  }
34465
34538
  var SUMI_PATH_SELECTORS, SUMI_PATH_KEYS, SUMI_PATH_OUTPUT_LIMIT, SumiPathResolverError;
34466
34539
  var init_agent_sumi = __esm(() => {
34540
+ init_hook_diagnostics();
34467
34541
  SUMI_PATH_SELECTORS = ["SUMI_HOME", "SUMI_CONFIG_DIR", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME"];
34468
34542
  SUMI_PATH_KEYS = ["data", "cache", "config", "state"];
34469
34543
  SUMI_PATH_OUTPUT_LIMIT = 128 * 1024;
package/bin/migrate.js CHANGED
@@ -25,7 +25,7 @@ import { join as join5 } from "path";
25
25
  // package.json
26
26
  var package_default = {
27
27
  name: "@hasna/skills",
28
- version: "0.10.64",
28
+ version: "0.10.65",
29
29
  description: "Skills library for AI coding agents",
30
30
  type: "module",
31
31
  bin: {
package/bin/server.js CHANGED
@@ -24140,7 +24140,7 @@ restoreLaunchCwd();
24140
24140
  // package.json
24141
24141
  var package_default = {
24142
24142
  name: "@hasna/skills",
24143
- version: "0.10.64",
24143
+ version: "0.10.65",
24144
24144
  description: "Skills library for AI coding agents",
24145
24145
  type: "module",
24146
24146
  bin: {
package/bin/worker.js CHANGED
@@ -24143,7 +24143,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24143
24143
  // package.json
24144
24144
  var package_default = {
24145
24145
  name: "@hasna/skills",
24146
- version: "0.10.64",
24146
+ version: "0.10.65",
24147
24147
  description: "Skills library for AI coding agents",
24148
24148
  type: "module",
24149
24149
  bin: {
package/dist/index.js CHANGED
@@ -11888,7 +11888,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
11888
11888
  });
11889
11889
  }
11890
11890
  // package.json
11891
- var version = "0.10.64";
11891
+ var version = "0.10.65";
11892
11892
  var package_default = {
11893
11893
  name: "@hasna/skills",
11894
11894
  version,
@@ -17141,6 +17141,78 @@ function disableCodexBundledSkills(text2) {
17141
17141
  import { spawnSync as spawnSync4 } from "child_process";
17142
17142
  import { accessSync as accessSync2, constants as constants11, existsSync as existsSync8, lstatSync as lstatSync19, realpathSync as realpathSync10, statSync as statSync9 } from "fs";
17143
17143
  import { delimiter as delimiter2, isAbsolute as isAbsolute15, join as join25, resolve as resolve15 } from "path";
17144
+
17145
+ // src/lib/hook-diagnostics.ts
17146
+ var CONTEXT_ERROR_CODES = new Set([
17147
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
17148
+ "SKILLS_API_UNAVAILABLE",
17149
+ "SKILLS_API_UNAUTHORIZED",
17150
+ "SKILLS_API_FORBIDDEN",
17151
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
17152
+ "SESSION_WRITE_BUSY",
17153
+ "SKILLS_CONTEXT_FAILED",
17154
+ "SKILLS_STORAGE_FULL",
17155
+ "PROFILE_LOCK_MISMATCH",
17156
+ "PROFILE_IDENTITY_MISMATCH",
17157
+ "PROFILE_APPLIED_REPORT_FAILED",
17158
+ "CACHED_AUTHORITY_REQUIRED",
17159
+ "CACHED_PROFILE_EXPIRED",
17160
+ "CACHED_PROFILE_MISSING",
17161
+ "CACHED_BUNDLE_MISSING",
17162
+ "INVALID_CONTEXT_INPUT",
17163
+ "CONTEXT_INPUT_TOO_LARGE",
17164
+ "INVALID_CONTEXT_BUDGET",
17165
+ "INVALID_SELECTION",
17166
+ "INVALID_SELECTION_ALIASES",
17167
+ "INVALID_RECEIPT",
17168
+ "INVALID_CACHE_FILE",
17169
+ "UNSAFE_CACHE_PATH",
17170
+ "RECEIPT_TOO_LARGE",
17171
+ "BUNDLE_DIGEST_MISMATCH",
17172
+ "BUNDLE_IDENTITY_MISMATCH",
17173
+ "BUNDLE_UNAVAILABLE",
17174
+ "BUNDLE_TOO_LARGE",
17175
+ "SKILL_NOT_SELECTED",
17176
+ "INVALID_SESSION",
17177
+ "SESSION_PARENT_NOT_FOUND",
17178
+ "SESSION_PARENT_MISMATCH",
17179
+ "SESSION_NOT_FOUND",
17180
+ "SESSION_GENERATION_CHANGED",
17181
+ "SESSION_GENERATION_EXHAUSTED",
17182
+ "SESSION_PARENT_CHANGED",
17183
+ "SESSION_RECEIPT_CHANGED",
17184
+ "SESSION_RECONCILIATION_REQUIRED",
17185
+ "SESSION_RENEWAL_TIMEOUT",
17186
+ "SESSION_CONTEXT_CONFLICT",
17187
+ "SESSION_RECONCILIATION_INCOMPLETE",
17188
+ "SESSION_WRITE_LOCKED",
17189
+ "SESSION_WRITE_LOCK_CHANGED",
17190
+ "SESSION_LOCK_RECOVERY_INCOMPLETE"
17191
+ ]);
17192
+ var HOOK_REFUSAL_REASON_CODES = Object.freeze([
17193
+ ...CONTEXT_ERROR_CODES,
17194
+ "SKILLS_HOOK_FAILED",
17195
+ "SKILLS_HOOK_TIMEOUT",
17196
+ "SKILLS_HOOK_INVALID_RESPONSE"
17197
+ ]);
17198
+ var OPTIONAL_CONTEXT_FAILURES = new Set([
17199
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
17200
+ "SKILLS_API_UNAVAILABLE",
17201
+ "SKILLS_API_UNAUTHORIZED",
17202
+ "SKILLS_API_FORBIDDEN",
17203
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
17204
+ "CACHED_PROFILE_EXPIRED",
17205
+ "CACHED_PROFILE_MISSING",
17206
+ "CACHED_BUNDLE_MISSING",
17207
+ "SESSION_RECONCILIATION_REQUIRED",
17208
+ "SESSION_RENEWAL_TIMEOUT",
17209
+ "SESSION_WRITE_BUSY",
17210
+ "SESSION_PARENT_CHANGED",
17211
+ "SESSION_CONTEXT_CONFLICT",
17212
+ "SKILLS_HOOK_TIMEOUT"
17213
+ ]);
17214
+
17215
+ // src/lib/agent-sumi.ts
17144
17216
  var SUMI_PATH_SELECTORS = ["SUMI_HOME", "SUMI_CONFIG_DIR", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME"];
17145
17217
  var SUMI_PATH_KEYS = ["data", "cache", "config", "state"];
17146
17218
  var SUMI_PATH_OUTPUT_LIMIT = 128 * 1024;
@@ -17282,24 +17354,33 @@ function sumiConfigPath(home, cwd = process.cwd(), env = process.env) {
17282
17354
  return join25(sumiConfigDirectory(home, env, cwd), "sumi.json");
17283
17355
  }
17284
17356
  function renderSumiPlugin(command, profile) {
17285
- return renderSumiPluginVersion(command, profile, false);
17357
+ return renderSumiPluginVersion(command, profile, 3);
17286
17358
  }
17287
17359
  function isManagedSumiPlugin(text2, command, profile) {
17288
- return text2 === renderSumiPlugin(command, profile) || text2 === renderSumiPluginVersion(command, profile, true);
17360
+ return text2 === renderSumiPlugin(command, profile) || text2 === renderSumiPluginVersion(command, profile, 2) || text2 === renderSumiPluginVersion(command, profile, 1);
17289
17361
  }
17290
- function renderSumiPluginVersion(command, profile, legacy) {
17362
+ function renderSumiPluginVersion(command, profile, version2) {
17363
+ const legacy = version2 === 1;
17291
17364
  const failure = (message) => legacy ? `new Error(${JSON.stringify(message)})` : "refusal()";
17292
17365
  return `// Managed by @hasna/skills. Regenerate with skills hook install --agent sumi.
17293
17366
  import { spawn } from "node:child_process";
17294
17367
  const command = ${JSON.stringify(command)};
17295
17368
  const profile = ${JSON.stringify(profile)};
17296
- ${legacy ? "" : `function refusal() {
17297
- const error = new Error("Skills verification blocked this request. Review the Sumi Skills hook configuration, then retry.");
17369
+ ${legacy ? "" : `function refusal(${version2 === 3 ? "reasonCode" : ""}) {
17370
+ const error = new Error("Skills verification blocked this request. Review the Sumi Skills hook configuration, then retry."${version2 === 3 ? ' + (reasonCode ? " [" + reasonCode + "]" : "")' : ""});
17298
17371
  error.name = "SkillsHookRefusal";
17299
- error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
17372
+ error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED"${version2 === 3 ? ", ...(reasonCode ? { reasonCode } : {})" : ""} };
17300
17373
  return error;
17301
17374
  }
17302
- `}function context(input) {
17375
+ `}${version2 === 3 ? `const reasonCodes = new Set(${JSON.stringify(HOOK_REFUSAL_REASON_CODES)});
17376
+ function refusedResult(result) {
17377
+ const reason = result.reason ?? result.stopReason ?? result.systemMessage;
17378
+ if (typeof reason !== "string" || reason.length > 16384) return refusal();
17379
+ if (reason.startsWith("NATIVE_SKILL_DRIFT:")) return refusal("NATIVE_SKILL_DRIFT");
17380
+ const code = /^Skills context is unavailable \\[([A-Z_]+)\\]/.exec(reason)?.[1];
17381
+ return refusal(reasonCodes.has(code) ? code : undefined);
17382
+ }
17383
+ ` : ""}function context(input) {
17303
17384
  return new Promise((resolve, reject) => {
17304
17385
  const child = spawn(command, ["hook", "user-prompt", "--agent", "sumi", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
17305
17386
  const chunks = []; let bytes = 0, failure;
@@ -17314,12 +17395,12 @@ ${legacy ? "" : `function refusal() {
17314
17395
  try {
17315
17396
  const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
17316
17397
  if (!result || typeof result !== "object" || Array.isArray(result)) throw ${failure("Invalid Skills hook response")};
17317
- if (result.decision === "block" || result.continue === false) throw ${failure("Skills prompt hook refused")};
17398
+ if (result.decision === "block" || result.continue === false) throw ${version2 === 3 ? "refusedResult(result)" : failure("Skills prompt hook refused")};
17318
17399
  if (Object.keys(result).length && (!result.hookSpecificOutput || result.hookSpecificOutput.hookEventName !== input.hook_event_name)) throw ${failure("Invalid Skills hook response")};
17319
17400
  const text = result.hookSpecificOutput?.additionalContext ?? "";
17320
17401
  if (typeof text !== "string") throw ${failure("Invalid Skills prompt context")};
17321
17402
  resolve(text);
17322
- } catch (error) { reject(${legacy ? "error" : "refusal()"}); }
17403
+ } catch (error) { reject(${legacy ? "error" : version2 === 3 ? 'error instanceof Error && error.name === "SkillsHookRefusal" ? error : refusal()' : "refusal()"}); }
17323
17404
  });
17324
17405
  child.stdin.end(JSON.stringify(input));
17325
17406
  });
@@ -17352,9 +17433,13 @@ export default {
17352
17433
  sessions.add(event.sessionID);
17353
17434
  }));
17354
17435
  registrations.push(await ctx.session.hook("context", async event => {
17355
- const last = event.messages.findLast(message => message.role === "user");
17436
+ ${version2 === 3 ? ` // Model messages also contain checkpoints, attachments and synthetic
17437
+ // user-role history, with no owner-prompt provenance. Prompt admission
17438
+ // already matched and persisted Skills; restore that verified state only.
17439
+ const prompt = "";
17440
+ ` : ` const last = event.messages.findLast(message => message.role === "user");
17356
17441
  const prompt = (last?.content ?? []).filter(part => part.type === "text").map(part => part.text).join("\\n");
17357
- // Recheck on every primary request, including resumed and synthetic
17442
+ `} // Recheck on every primary request, including resumed and synthetic
17358
17443
  // sessions. The CLI owns optional failures versus explicit refusals.
17359
17444
  const text = await context(await input(event.sessionID, prompt, !sessions.has(event.sessionID)));
17360
17445
  if (text) event.system.push({ type: "text", text });
@@ -1,3 +1,5 @@
1
+ /** Fixed codes only; generated native adapters must never copy refusal prose. */
2
+ export declare const HOOK_REFUSAL_REASON_CODES: readonly string[];
1
3
  export declare class HookDiagnosticError extends Error {
2
4
  readonly code: string;
3
5
  readonly stage: "context" | "sync";
@@ -49,7 +49,7 @@ var __require = import.meta.require;
49
49
  // src/runtime-prerequisites.ts
50
50
  import { isAbsolute as isAbsolute5 } from "path";
51
51
  // package.json
52
- var version = "0.10.64";
52
+ var version = "0.10.65";
53
53
  var package_default = {
54
54
  name: "@hasna/skills",
55
55
  version,
@@ -2864,6 +2864,78 @@ function detectedIntegrationAgents(options = {}) {
2864
2864
  import { spawnSync as spawnSync2 } from "child_process";
2865
2865
  import { accessSync, constants as constants4, existsSync as existsSync4, lstatSync as lstatSync5, realpathSync, statSync as statSync2 } from "fs";
2866
2866
  import { delimiter, isAbsolute as isAbsolute4, join as join8, resolve as resolve7 } from "path";
2867
+
2868
+ // src/lib/hook-diagnostics.ts
2869
+ var CONTEXT_ERROR_CODES = new Set([
2870
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
2871
+ "SKILLS_API_UNAVAILABLE",
2872
+ "SKILLS_API_UNAUTHORIZED",
2873
+ "SKILLS_API_FORBIDDEN",
2874
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
2875
+ "SESSION_WRITE_BUSY",
2876
+ "SKILLS_CONTEXT_FAILED",
2877
+ "SKILLS_STORAGE_FULL",
2878
+ "PROFILE_LOCK_MISMATCH",
2879
+ "PROFILE_IDENTITY_MISMATCH",
2880
+ "PROFILE_APPLIED_REPORT_FAILED",
2881
+ "CACHED_AUTHORITY_REQUIRED",
2882
+ "CACHED_PROFILE_EXPIRED",
2883
+ "CACHED_PROFILE_MISSING",
2884
+ "CACHED_BUNDLE_MISSING",
2885
+ "INVALID_CONTEXT_INPUT",
2886
+ "CONTEXT_INPUT_TOO_LARGE",
2887
+ "INVALID_CONTEXT_BUDGET",
2888
+ "INVALID_SELECTION",
2889
+ "INVALID_SELECTION_ALIASES",
2890
+ "INVALID_RECEIPT",
2891
+ "INVALID_CACHE_FILE",
2892
+ "UNSAFE_CACHE_PATH",
2893
+ "RECEIPT_TOO_LARGE",
2894
+ "BUNDLE_DIGEST_MISMATCH",
2895
+ "BUNDLE_IDENTITY_MISMATCH",
2896
+ "BUNDLE_UNAVAILABLE",
2897
+ "BUNDLE_TOO_LARGE",
2898
+ "SKILL_NOT_SELECTED",
2899
+ "INVALID_SESSION",
2900
+ "SESSION_PARENT_NOT_FOUND",
2901
+ "SESSION_PARENT_MISMATCH",
2902
+ "SESSION_NOT_FOUND",
2903
+ "SESSION_GENERATION_CHANGED",
2904
+ "SESSION_GENERATION_EXHAUSTED",
2905
+ "SESSION_PARENT_CHANGED",
2906
+ "SESSION_RECEIPT_CHANGED",
2907
+ "SESSION_RECONCILIATION_REQUIRED",
2908
+ "SESSION_RENEWAL_TIMEOUT",
2909
+ "SESSION_CONTEXT_CONFLICT",
2910
+ "SESSION_RECONCILIATION_INCOMPLETE",
2911
+ "SESSION_WRITE_LOCKED",
2912
+ "SESSION_WRITE_LOCK_CHANGED",
2913
+ "SESSION_LOCK_RECOVERY_INCOMPLETE"
2914
+ ]);
2915
+ var HOOK_REFUSAL_REASON_CODES = Object.freeze([
2916
+ ...CONTEXT_ERROR_CODES,
2917
+ "SKILLS_HOOK_FAILED",
2918
+ "SKILLS_HOOK_TIMEOUT",
2919
+ "SKILLS_HOOK_INVALID_RESPONSE"
2920
+ ]);
2921
+ var OPTIONAL_CONTEXT_FAILURES = new Set([
2922
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
2923
+ "SKILLS_API_UNAVAILABLE",
2924
+ "SKILLS_API_UNAUTHORIZED",
2925
+ "SKILLS_API_FORBIDDEN",
2926
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
2927
+ "CACHED_PROFILE_EXPIRED",
2928
+ "CACHED_PROFILE_MISSING",
2929
+ "CACHED_BUNDLE_MISSING",
2930
+ "SESSION_RECONCILIATION_REQUIRED",
2931
+ "SESSION_RENEWAL_TIMEOUT",
2932
+ "SESSION_WRITE_BUSY",
2933
+ "SESSION_PARENT_CHANGED",
2934
+ "SESSION_CONTEXT_CONFLICT",
2935
+ "SKILLS_HOOK_TIMEOUT"
2936
+ ]);
2937
+
2938
+ // src/lib/agent-sumi.ts
2867
2939
  var SUMI_PATH_SELECTORS = ["SUMI_HOME", "SUMI_CONFIG_DIR", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME"];
2868
2940
  var SUMI_PATH_KEYS = ["data", "cache", "config", "state"];
2869
2941
  var SUMI_PATH_OUTPUT_LIMIT = 128 * 1024;
@@ -3005,24 +3077,33 @@ function sumiConfigPath(home, cwd = process.cwd(), env = process.env) {
3005
3077
  return join8(sumiConfigDirectory(home, env, cwd), "sumi.json");
3006
3078
  }
3007
3079
  function renderSumiPlugin(command, profile) {
3008
- return renderSumiPluginVersion(command, profile, false);
3080
+ return renderSumiPluginVersion(command, profile, 3);
3009
3081
  }
3010
3082
  function isManagedSumiPlugin(text2, command, profile) {
3011
- return text2 === renderSumiPlugin(command, profile) || text2 === renderSumiPluginVersion(command, profile, true);
3083
+ return text2 === renderSumiPlugin(command, profile) || text2 === renderSumiPluginVersion(command, profile, 2) || text2 === renderSumiPluginVersion(command, profile, 1);
3012
3084
  }
3013
- function renderSumiPluginVersion(command, profile, legacy) {
3085
+ function renderSumiPluginVersion(command, profile, version2) {
3086
+ const legacy = version2 === 1;
3014
3087
  const failure = (message) => legacy ? `new Error(${JSON.stringify(message)})` : "refusal()";
3015
3088
  return `// Managed by @hasna/skills. Regenerate with skills hook install --agent sumi.
3016
3089
  import { spawn } from "node:child_process";
3017
3090
  const command = ${JSON.stringify(command)};
3018
3091
  const profile = ${JSON.stringify(profile)};
3019
- ${legacy ? "" : `function refusal() {
3020
- const error = new Error("Skills verification blocked this request. Review the Sumi Skills hook configuration, then retry.");
3092
+ ${legacy ? "" : `function refusal(${version2 === 3 ? "reasonCode" : ""}) {
3093
+ const error = new Error("Skills verification blocked this request. Review the Sumi Skills hook configuration, then retry."${version2 === 3 ? ' + (reasonCode ? " [" + reasonCode + "]" : "")' : ""});
3021
3094
  error.name = "SkillsHookRefusal";
3022
- error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
3095
+ error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED"${version2 === 3 ? ", ...(reasonCode ? { reasonCode } : {})" : ""} };
3023
3096
  return error;
3024
3097
  }
3025
- `}function context(input) {
3098
+ `}${version2 === 3 ? `const reasonCodes = new Set(${JSON.stringify(HOOK_REFUSAL_REASON_CODES)});
3099
+ function refusedResult(result) {
3100
+ const reason = result.reason ?? result.stopReason ?? result.systemMessage;
3101
+ if (typeof reason !== "string" || reason.length > 16384) return refusal();
3102
+ if (reason.startsWith("NATIVE_SKILL_DRIFT:")) return refusal("NATIVE_SKILL_DRIFT");
3103
+ const code = /^Skills context is unavailable \\[([A-Z_]+)\\]/.exec(reason)?.[1];
3104
+ return refusal(reasonCodes.has(code) ? code : undefined);
3105
+ }
3106
+ ` : ""}function context(input) {
3026
3107
  return new Promise((resolve, reject) => {
3027
3108
  const child = spawn(command, ["hook", "user-prompt", "--agent", "sumi", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
3028
3109
  const chunks = []; let bytes = 0, failure;
@@ -3037,12 +3118,12 @@ ${legacy ? "" : `function refusal() {
3037
3118
  try {
3038
3119
  const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
3039
3120
  if (!result || typeof result !== "object" || Array.isArray(result)) throw ${failure("Invalid Skills hook response")};
3040
- if (result.decision === "block" || result.continue === false) throw ${failure("Skills prompt hook refused")};
3121
+ if (result.decision === "block" || result.continue === false) throw ${version2 === 3 ? "refusedResult(result)" : failure("Skills prompt hook refused")};
3041
3122
  if (Object.keys(result).length && (!result.hookSpecificOutput || result.hookSpecificOutput.hookEventName !== input.hook_event_name)) throw ${failure("Invalid Skills hook response")};
3042
3123
  const text = result.hookSpecificOutput?.additionalContext ?? "";
3043
3124
  if (typeof text !== "string") throw ${failure("Invalid Skills prompt context")};
3044
3125
  resolve(text);
3045
- } catch (error) { reject(${legacy ? "error" : "refusal()"}); }
3126
+ } catch (error) { reject(${legacy ? "error" : version2 === 3 ? 'error instanceof Error && error.name === "SkillsHookRefusal" ? error : refusal()' : "refusal()"}); }
3046
3127
  });
3047
3128
  child.stdin.end(JSON.stringify(input));
3048
3129
  });
@@ -3075,9 +3156,13 @@ export default {
3075
3156
  sessions.add(event.sessionID);
3076
3157
  }));
3077
3158
  registrations.push(await ctx.session.hook("context", async event => {
3078
- const last = event.messages.findLast(message => message.role === "user");
3159
+ ${version2 === 3 ? ` // Model messages also contain checkpoints, attachments and synthetic
3160
+ // user-role history, with no owner-prompt provenance. Prompt admission
3161
+ // already matched and persisted Skills; restore that verified state only.
3162
+ const prompt = "";
3163
+ ` : ` const last = event.messages.findLast(message => message.role === "user");
3079
3164
  const prompt = (last?.content ?? []).filter(part => part.type === "text").map(part => part.text).join("\\n");
3080
- // Recheck on every primary request, including resumed and synthetic
3165
+ `} // Recheck on every primary request, including resumed and synthetic
3081
3166
  // sessions. The CLI owns optional failures versus explicit refusals.
3082
3167
  const text = await context(await input(event.sessionID, prompt, !sessions.has(event.sessionID)));
3083
3168
  if (text) event.system.push({ type: "text", text });
package/dist/sdk/index.js CHANGED
@@ -32371,7 +32371,7 @@ var init_event_sink = __esm(() => {
32371
32371
  init_dist();
32372
32372
  });
32373
32373
  // package.json
32374
- var version = "0.10.64";
32374
+ var version = "0.10.65";
32375
32375
  var package_default = {
32376
32376
  name: "@hasna/skills",
32377
32377
  version,
@@ -44349,6 +44349,78 @@ function verifiesCodexHookDiscoverySource(binding, source, currentDigest, witnes
44349
44349
  import { spawnSync as spawnSync5 } from "child_process";
44350
44350
  import { accessSync as accessSync2, constants as constants16, existsSync as existsSync6, lstatSync as lstatSync22, realpathSync as realpathSync10, statSync as statSync7 } from "fs";
44351
44351
  import { delimiter as delimiter2, isAbsolute as isAbsolute21, join as join27, resolve as resolve20 } from "path";
44352
+
44353
+ // src/lib/hook-diagnostics.ts
44354
+ var CONTEXT_ERROR_CODES = new Set([
44355
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
44356
+ "SKILLS_API_UNAVAILABLE",
44357
+ "SKILLS_API_UNAUTHORIZED",
44358
+ "SKILLS_API_FORBIDDEN",
44359
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
44360
+ "SESSION_WRITE_BUSY",
44361
+ "SKILLS_CONTEXT_FAILED",
44362
+ "SKILLS_STORAGE_FULL",
44363
+ "PROFILE_LOCK_MISMATCH",
44364
+ "PROFILE_IDENTITY_MISMATCH",
44365
+ "PROFILE_APPLIED_REPORT_FAILED",
44366
+ "CACHED_AUTHORITY_REQUIRED",
44367
+ "CACHED_PROFILE_EXPIRED",
44368
+ "CACHED_PROFILE_MISSING",
44369
+ "CACHED_BUNDLE_MISSING",
44370
+ "INVALID_CONTEXT_INPUT",
44371
+ "CONTEXT_INPUT_TOO_LARGE",
44372
+ "INVALID_CONTEXT_BUDGET",
44373
+ "INVALID_SELECTION",
44374
+ "INVALID_SELECTION_ALIASES",
44375
+ "INVALID_RECEIPT",
44376
+ "INVALID_CACHE_FILE",
44377
+ "UNSAFE_CACHE_PATH",
44378
+ "RECEIPT_TOO_LARGE",
44379
+ "BUNDLE_DIGEST_MISMATCH",
44380
+ "BUNDLE_IDENTITY_MISMATCH",
44381
+ "BUNDLE_UNAVAILABLE",
44382
+ "BUNDLE_TOO_LARGE",
44383
+ "SKILL_NOT_SELECTED",
44384
+ "INVALID_SESSION",
44385
+ "SESSION_PARENT_NOT_FOUND",
44386
+ "SESSION_PARENT_MISMATCH",
44387
+ "SESSION_NOT_FOUND",
44388
+ "SESSION_GENERATION_CHANGED",
44389
+ "SESSION_GENERATION_EXHAUSTED",
44390
+ "SESSION_PARENT_CHANGED",
44391
+ "SESSION_RECEIPT_CHANGED",
44392
+ "SESSION_RECONCILIATION_REQUIRED",
44393
+ "SESSION_RENEWAL_TIMEOUT",
44394
+ "SESSION_CONTEXT_CONFLICT",
44395
+ "SESSION_RECONCILIATION_INCOMPLETE",
44396
+ "SESSION_WRITE_LOCKED",
44397
+ "SESSION_WRITE_LOCK_CHANGED",
44398
+ "SESSION_LOCK_RECOVERY_INCOMPLETE"
44399
+ ]);
44400
+ var HOOK_REFUSAL_REASON_CODES = Object.freeze([
44401
+ ...CONTEXT_ERROR_CODES,
44402
+ "SKILLS_HOOK_FAILED",
44403
+ "SKILLS_HOOK_TIMEOUT",
44404
+ "SKILLS_HOOK_INVALID_RESPONSE"
44405
+ ]);
44406
+ var OPTIONAL_CONTEXT_FAILURES = new Set([
44407
+ "SKILLS_API_CREDENTIAL_UNAVAILABLE",
44408
+ "SKILLS_API_UNAVAILABLE",
44409
+ "SKILLS_API_UNAUTHORIZED",
44410
+ "SKILLS_API_FORBIDDEN",
44411
+ "SKILLS_API_RESOURCE_UNAVAILABLE",
44412
+ "CACHED_PROFILE_EXPIRED",
44413
+ "CACHED_PROFILE_MISSING",
44414
+ "CACHED_BUNDLE_MISSING",
44415
+ "SESSION_RECONCILIATION_REQUIRED",
44416
+ "SESSION_RENEWAL_TIMEOUT",
44417
+ "SESSION_WRITE_BUSY",
44418
+ "SESSION_PARENT_CHANGED",
44419
+ "SESSION_CONTEXT_CONFLICT",
44420
+ "SKILLS_HOOK_TIMEOUT"
44421
+ ]);
44422
+
44423
+ // src/lib/agent-sumi.ts
44352
44424
  var SUMI_PATH_SELECTORS = ["SUMI_HOME", "SUMI_CONFIG_DIR", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME"];
44353
44425
  var SUMI_PATH_KEYS = ["data", "cache", "config", "state"];
44354
44426
  var SUMI_PATH_OUTPUT_LIMIT = 128 * 1024;
@@ -44490,24 +44562,33 @@ function sumiConfigPath(home, cwd = process.cwd(), env = process.env) {
44490
44562
  return join27(sumiConfigDirectory(home, env, cwd), "sumi.json");
44491
44563
  }
44492
44564
  function renderSumiPlugin(command, profile) {
44493
- return renderSumiPluginVersion(command, profile, false);
44565
+ return renderSumiPluginVersion(command, profile, 3);
44494
44566
  }
44495
44567
  function isManagedSumiPlugin(text2, command, profile) {
44496
- return text2 === renderSumiPlugin(command, profile) || text2 === renderSumiPluginVersion(command, profile, true);
44568
+ return text2 === renderSumiPlugin(command, profile) || text2 === renderSumiPluginVersion(command, profile, 2) || text2 === renderSumiPluginVersion(command, profile, 1);
44497
44569
  }
44498
- function renderSumiPluginVersion(command, profile, legacy) {
44570
+ function renderSumiPluginVersion(command, profile, version2) {
44571
+ const legacy = version2 === 1;
44499
44572
  const failure = (message) => legacy ? `new Error(${JSON.stringify(message)})` : "refusal()";
44500
44573
  return `// Managed by @hasna/skills. Regenerate with skills hook install --agent sumi.
44501
44574
  import { spawn } from "node:child_process";
44502
44575
  const command = ${JSON.stringify(command)};
44503
44576
  const profile = ${JSON.stringify(profile)};
44504
- ${legacy ? "" : `function refusal() {
44505
- const error = new Error("Skills verification blocked this request. Review the Sumi Skills hook configuration, then retry.");
44577
+ ${legacy ? "" : `function refusal(${version2 === 3 ? "reasonCode" : ""}) {
44578
+ const error = new Error("Skills verification blocked this request. Review the Sumi Skills hook configuration, then retry."${version2 === 3 ? ' + (reasonCode ? " [" + reasonCode + "]" : "")' : ""});
44506
44579
  error.name = "SkillsHookRefusal";
44507
- error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
44580
+ error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED"${version2 === 3 ? ", ...(reasonCode ? { reasonCode } : {})" : ""} };
44508
44581
  return error;
44509
44582
  }
44510
- `}function context(input) {
44583
+ `}${version2 === 3 ? `const reasonCodes = new Set(${JSON.stringify(HOOK_REFUSAL_REASON_CODES)});
44584
+ function refusedResult(result) {
44585
+ const reason = result.reason ?? result.stopReason ?? result.systemMessage;
44586
+ if (typeof reason !== "string" || reason.length > 16384) return refusal();
44587
+ if (reason.startsWith("NATIVE_SKILL_DRIFT:")) return refusal("NATIVE_SKILL_DRIFT");
44588
+ const code = /^Skills context is unavailable \\[([A-Z_]+)\\]/.exec(reason)?.[1];
44589
+ return refusal(reasonCodes.has(code) ? code : undefined);
44590
+ }
44591
+ ` : ""}function context(input) {
44511
44592
  return new Promise((resolve, reject) => {
44512
44593
  const child = spawn(command, ["hook", "user-prompt", "--agent", "sumi", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
44513
44594
  const chunks = []; let bytes = 0, failure;
@@ -44522,12 +44603,12 @@ ${legacy ? "" : `function refusal() {
44522
44603
  try {
44523
44604
  const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
44524
44605
  if (!result || typeof result !== "object" || Array.isArray(result)) throw ${failure("Invalid Skills hook response")};
44525
- if (result.decision === "block" || result.continue === false) throw ${failure("Skills prompt hook refused")};
44606
+ if (result.decision === "block" || result.continue === false) throw ${version2 === 3 ? "refusedResult(result)" : failure("Skills prompt hook refused")};
44526
44607
  if (Object.keys(result).length && (!result.hookSpecificOutput || result.hookSpecificOutput.hookEventName !== input.hook_event_name)) throw ${failure("Invalid Skills hook response")};
44527
44608
  const text = result.hookSpecificOutput?.additionalContext ?? "";
44528
44609
  if (typeof text !== "string") throw ${failure("Invalid Skills prompt context")};
44529
44610
  resolve(text);
44530
- } catch (error) { reject(${legacy ? "error" : "refusal()"}); }
44611
+ } catch (error) { reject(${legacy ? "error" : version2 === 3 ? 'error instanceof Error && error.name === "SkillsHookRefusal" ? error : refusal()' : "refusal()"}); }
44531
44612
  });
44532
44613
  child.stdin.end(JSON.stringify(input));
44533
44614
  });
@@ -44560,9 +44641,13 @@ export default {
44560
44641
  sessions.add(event.sessionID);
44561
44642
  }));
44562
44643
  registrations.push(await ctx.session.hook("context", async event => {
44563
- const last = event.messages.findLast(message => message.role === "user");
44644
+ ${version2 === 3 ? ` // Model messages also contain checkpoints, attachments and synthetic
44645
+ // user-role history, with no owner-prompt provenance. Prompt admission
44646
+ // already matched and persisted Skills; restore that verified state only.
44647
+ const prompt = "";
44648
+ ` : ` const last = event.messages.findLast(message => message.role === "user");
44564
44649
  const prompt = (last?.content ?? []).filter(part => part.type === "text").map(part => part.text).join("\\n");
44565
- // Recheck on every primary request, including resumed and synthetic
44650
+ `} // Recheck on every primary request, including resumed and synthetic
44566
44651
  // sessions. The CLI owns optional failures versus explicit refusals.
44567
44652
  const text = await context(await input(event.sessionID, prompt, !sessions.has(event.sessionID)));
44568
44653
  if (text) event.system.push({ type: "text", text });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.64",
3
+ "version": "0.10.65",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {