@ainova-systems/intelligence 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- 1bf045d314f2c2f58a70d5a74130b5f5376b56e4
1
+ 7dfbb01c2182f3de05bff94f3e5e080cf267f8c6
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.18.0
1
+ 0.18.1
@@ -677,12 +677,23 @@ sync_open_skill_dirs() {
677
677
  # `agents/openai.yaml` beside SKILL.md. Source skills stay tool-neutral: the
678
678
  # policy file is derived here, in the open-standard tree Codex reads, so every
679
679
  # adapter sharing that tree writes the same bytes. A skill that ships its own
680
- # `agents/openai.yaml` keeps it — that is the author configuring Codex directly.
680
+ # `agents/openai.yaml` keeps the rest of it: the output copy gains the policy
681
+ # when the author's file sets none, and a file whose policy says otherwise, or
682
+ # that sync cannot read as saying it, refuses the render — an owner-only skill
683
+ # Codex can still select is the exact failure the field exists to prevent, so
684
+ # it never passes as a success.
681
685
  open_skill_invocation_policies() {
682
- local output_dir="$1" path flag rows
683
- local -a skill_mds=() policy_dirs=() policies=()
686
+ local output_dir="$1" path dir flag rows refusals
687
+ local -a skill_mds=() policy_dirs=() policies=() authored=()
684
688
  for path in "$output_dir"/*/SKILL.md; do
685
- [ -f "$path" ] && [ ! -L "$path" ] && skill_mds+=("$path")
689
+ dir="${path%/SKILL.md}"
690
+ # A link is emitted as-is (see _skill_bundle_note): reading through it
691
+ # or writing beside it would reach outside the output tree.
692
+ if [ -L "$dir" ] || [ -L "$path" ]; then
693
+ echo " WARN: ${dir##*/}/SKILL.md is reached through a symlink — Codex gets no invocation policy derived for it" >&2
694
+ elif [ -f "$path" ]; then
695
+ skill_mds+=("$path")
696
+ fi
686
697
  done
687
698
  [ "${#skill_mds[@]}" -gt 0 ] || return 0
688
699
  # Captured, not streamed: a reader cut short must fail the render rather
@@ -693,13 +704,32 @@ open_skill_invocation_policies() {
693
704
  }
694
705
  while IFS=$'\x1f' read -r path flag; do
695
706
  [ "$flag" = "true" ] || continue
696
- path="${path%/SKILL.md}/agents"
697
- # A symlinked `agents` would carry the write outside the output tree.
698
- [ -L "$path" ] && continue
699
- [ -e "$path/openai.yaml" ] || [ -L "$path/openai.yaml" ] && continue
707
+ dir="${path%/SKILL.md}"
708
+ path="$dir/agents"
709
+ if [ -L "$path" ] || [ -L "$path/openai.yaml" ]; then
710
+ echo " WARN: ${dir##*/}/agents/openai.yaml is a symlink — left as-is, its invocation policy is not enforced" >&2
711
+ continue
712
+ fi
713
+ if [ -s "$path/openai.yaml" ]; then
714
+ authored+=("$path/openai.yaml")
715
+ continue
716
+ fi
700
717
  policy_dirs+=("$path")
701
718
  policies+=("$path/openai.yaml")
702
719
  done <<< "$rows"
720
+ if [ "${#authored[@]}" -gt 0 ]; then
721
+ refusals="$(enforce_authored_invocation_policies "${authored[@]}")" || {
722
+ echo "ERROR: could not read or rewrite agents/openai.yaml under $output_dir" >&2
723
+ return 1
724
+ }
725
+ if [ -n "$refusals" ]; then
726
+ while IFS=$'\t' read -r path flag; do
727
+ path="${path%/agents/openai.yaml}"
728
+ echo "ERROR: ${path##*/} sets disable-model-invocation: true, but its agents/openai.yaml $flag — make them agree, or Codex can still select the skill" >&2
729
+ done <<< "$refusals"
730
+ return 1
731
+ fi
732
+ fi
703
733
  [ "${#policies[@]}" -gt 0 ] || return 0
704
734
  mkdir -p "${policy_dirs[@]}"
705
735
  for path in "${policies[@]}"; do
@@ -709,6 +739,94 @@ open_skill_invocation_policies() {
709
739
  finalize_output_files "${policies[@]}"
710
740
  }
711
741
 
742
+ # enforce_authored_invocation_policies <openai.yaml>... — one awk pass over
743
+ # author-owned Codex files of skills that set `disable-model-invocation: true`.
744
+ # A file already setting `policy.allow_implicit_invocation: false` is left
745
+ # alone; one without that key gains it, inside an existing `policy:` block or
746
+ # as a new block, every other line kept. Only the plain boolean on a direct
747
+ # child of `policy:` counts — the key Codex reads. A file that sets it
748
+ # otherwise, or whose `policy:` sync cannot read or extend safely, is printed
749
+ # as `<file>\t<reason>` and left untouched for the caller to refuse.
750
+ enforce_authored_invocation_policies() {
751
+ awk '
752
+ function reset() {
753
+ file = ""; n = 0; haspol = 0; inpol = 0; polline = 0
754
+ inline = ""; child = ""; val = ""; hasval = 0; bad = ""
755
+ }
756
+ # A one-line policy counts only as a flat flow mapping of plain
757
+ # scalars holding the key itself; a quoted or nested value is not read.
758
+ function inline_ok(s, body, parts, k, i, p) {
759
+ if (s !~ /^\{.*\}$/) return 0
760
+ body = substr(s, 2, length(s) - 2)
761
+ if (body ~ /[{}"\047]/ || index(body, "[") || index(body, "]")) return 0
762
+ k = split(body, parts, ",")
763
+ for (i = 1; i <= k; i++) {
764
+ p = parts[i]
765
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", p)
766
+ if (p ~ /^allow_implicit_invocation[[:space:]]*:[[:space:]]*false$/) return 1
767
+ }
768
+ return 0
769
+ }
770
+ function finish( i, ind) {
771
+ if (file == "") return
772
+ if (bad == "" && inline != "" && !inline_ok(inline))
773
+ bad = "has policy: " inline ", which does not plainly set allow_implicit_invocation: false"
774
+ if (bad == "" && hasval && val != "false")
775
+ bad = "sets policy.allow_implicit_invocation: " (val == "" ? "null" : val)
776
+ if (bad != "") { printf "%s\t%s\n", file, bad; reset(); return }
777
+ if (inline != "" || hasval) { reset(); return }
778
+ ind = (child != "") ? child : " "
779
+ for (i = 1; i <= n; i++) {
780
+ print L[i] > file
781
+ if (haspol && i == polline) print ind "allow_implicit_invocation: false" > file
782
+ }
783
+ if (!haspol) {
784
+ print "policy:" > file
785
+ print " allow_implicit_invocation: false" > file
786
+ }
787
+ close(file)
788
+ reset()
789
+ }
790
+ BEGIN { reset() }
791
+ FNR == 1 { finish(); file = FILENAME }
792
+ { sub(/\r$/, ""); L[++n] = $0 }
793
+ /^["\047]?policy["\047]?[[:space:]]*:/ {
794
+ if (haspol) bad = "has more than one policy: key"
795
+ haspol = 1; polline = n; inpol = 1
796
+ rest = $0
797
+ sub(/^[^:]*:[[:space:]]*/, "", rest)
798
+ sub(/[[:space:]]+#.*$/, "", rest)
799
+ sub(/^#.*$/, "", rest)
800
+ sub(/[[:space:]]+$/, "", rest)
801
+ if (rest != "") { inline = rest; inpol = 0 }
802
+ next
803
+ }
804
+ inpol && /^[^[:space:]#]/ { inpol = 0 }
805
+ # The first indented line fixes the depth of the direct children of
806
+ # policy:, and a deeper line belongs to another key, not the one Codex
807
+ # reads.
808
+ inpol && /^[[:space:]]+[^[:space:]#]/ {
809
+ match($0, /^[[:space:]]+/)
810
+ cur = substr($0, 1, RLENGTH)
811
+ if (child == "") {
812
+ child = cur
813
+ if ($0 ~ /^[[:space:]]+-([[:space:]]|$)/) bad = "has a policy: that is not a mapping"
814
+ } else if (length(cur) < length(child)) {
815
+ bad = "has a policy: block indented inconsistently"
816
+ }
817
+ if (cur == child && $0 ~ /^[[:space:]]+["\047]?allow_implicit_invocation["\047]?[[:space:]]*:/) {
818
+ v = $0
819
+ sub(/^[^:]*:[[:space:]]*/, "", v)
820
+ sub(/[[:space:]]+#.*$/, "", v)
821
+ sub(/^#.*$/, "", v)
822
+ sub(/[[:space:]]+$/, "", v)
823
+ hasval = 1; val = v
824
+ }
825
+ }
826
+ END { finish() }
827
+ ' "$@"
828
+ }
829
+
712
830
  # Lint YAML frontmatter for common pitfalls (unquoted colons, leading tabs).
713
831
  # Print warnings to stderr; do not fail. Strict consumers (Codex CLI) reject
714
832
  # these files with cryptic messages — catching them in sync gives better DX.
@@ -1010,7 +1128,11 @@ load_model_tiers() {
1010
1128
  }
1011
1129
 
1012
1130
  # resolve_model_var <tier> — set IS_MODEL from the tiers load_model_tiers
1013
- # resolved. An empty tier resolves to heavy, like get_model's default.
1131
+ # resolved. An empty tier resolves to heavy, like get_model's default. A tier
1132
+ # the tool has no default for and the manifest overrides nowhere — a typo, as a
1133
+ # rule — still renders an empty model, but never silently: the run warns once
1134
+ # per tool and tier, unindented on purpose because `sync --compact` keeps only
1135
+ # `WARNING:` lines.
1014
1136
  # shellcheck disable=SC2034 # IS_MODEL is the return channel read by adapters
1015
1137
  resolve_model_var() {
1016
1138
  case "$1" in
@@ -1018,7 +1140,17 @@ resolve_model_var() {
1018
1140
  heavy|"") IS_MODEL="$IS_MODEL_HEAVY" ;;
1019
1141
  standard) IS_MODEL="$IS_MODEL_STANDARD" ;;
1020
1142
  light) IS_MODEL="$IS_MODEL_LIGHT" ;;
1021
- *) IS_MODEL="$(get_model "$IS_MODEL_CFG" "$IS_MODEL_IDE" "$1")" ;;
1143
+ *)
1144
+ IS_MODEL="$(get_model "$IS_MODEL_CFG" "$IS_MODEL_IDE" "$1")"
1145
+ [ -n "$IS_MODEL" ] && return 0
1146
+ case "${IS_MODEL_WARNED:-|}" in
1147
+ *"|$IS_MODEL_IDE:$1|"*) ;;
1148
+ *)
1149
+ IS_MODEL_WARNED="${IS_MODEL_WARNED:-|}$IS_MODEL_IDE:$1|"
1150
+ echo "WARNING: no $IS_MODEL_IDE model for tier '$1' — agents with it get an empty model; use frontier, heavy, standard or light, or set models.$IS_MODEL_IDE.$1" >&2
1151
+ ;;
1152
+ esac
1153
+ ;;
1022
1154
  esac
1023
1155
  }
1024
1156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Build, version and distribute AI agent intelligence across your organization — one CLI, versioned Intelligence Packages, and a sync engine for Claude Code, Cursor, Copilot, Codex, Pi and OpenCode.",
5
5
  "bin": {
6
6
  "intelligence": "bin/intelligence.js"
@@ -194,7 +194,7 @@ Do not instruct an agent to read rules or restate their content. Claude loads it
194
194
 
195
195
  Codex also receives a reasoning effort per tier — `xhigh`, `high`, `medium`, `low` — which is what separates `frontier` from `heavy` in Codex. Copilot has no effort field, so its `frontier` and `heavy` agents are identical. Claude's Haiku 4.5 is retiring with no successor announced, so `light` shares `standard`'s Sonnet in Claude and OpenCode; Cursor and Antigravity have fewer native levels than there are tiers, so several tiers share one value there too.
196
196
 
197
- The vocabulary is tool-neutral. Adapters resolve it through `get_model()`. Override a default under `models.<tool>.<tier>` in `intelligence.yaml` only when the project needs a pin; sync reports drift when that override differs from the current default.
197
+ The vocabulary is tool-neutral. Adapters resolve it through `get_model()`. Override a default under `models.<tool>.<tier>` in `intelligence.yaml` only when the project needs a pin; sync reports drift when that override differs from the current default. Another tier name works only where `models.<tool>.<tier>` defines it; elsewhere — a typo, as a rule — the agent renders with an empty `model` and sync warns once per tool and tier.
198
198
 
199
199
  ### Access mappings
200
200
 
@@ -254,7 +254,7 @@ argument-hint: "<route-name>"
254
254
 
255
255
  Standard optional fields (`license`, `compatibility`, `metadata`, `allowed-tools`) and tool extensions pass through unchanged. A tool ignores fields it does not understand.
256
256
 
257
- `disable-model-invocation: true` makes a skill one only the owner starts, as a slash command: use it for a procedure that changes versions, the lock or generated outputs, where an agent selecting it unasked is the failure. Claude Code, Cursor and Copilot read the field itself; Codex does not, so sync writes `agents/openai.yaml` with `allow_implicit_invocation: false` beside the skill in `.agents/skills/`. Keep the source to the field — a skill that ships its own `agents/openai.yaml` keeps it unchanged.
257
+ `disable-model-invocation: true` makes a skill one only the owner starts, as a slash command: use it for a procedure that changes versions, the lock or generated outputs, where an agent selecting it unasked is the failure. Claude Code, Cursor and Copilot read the field itself; Codex does not, so sync writes `agents/openai.yaml` with `allow_implicit_invocation: false` beside the skill in `.agents/skills/`. Keep the source to the field. A skill that ships its own `agents/openai.yaml` keeps everything else in it: sync adds the policy when the file sets none, and refuses one that sets it otherwise or not as a plain `allow_implicit_invocation: false` directly under `policy:`.
258
258
 
259
259
  These limits reject a skill instead of degrading it:
260
260