@kontourai/flow-agents 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -285,7 +285,7 @@ else
285
285
  _fail "pi install failed"
286
286
  fi
287
287
 
288
- USER_SKILLS_DIR="$CODEX_FULL_DEST/.codex/sk""ills/user-skill"
288
+ USER_SKILLS_DIR="$CODEX_FULL_DEST/.agents/sk""ills/user-skill"
289
289
  mkdir -p "$CODEX_FULL_DEST/.codex/ag""ents" "$USER_SKILLS_DIR"
290
290
  printf 'name = "user-agent"\n' > "$CODEX_FULL_DEST/.codex/ag""ents/user-agent.toml"
291
291
  printf '# user skill\n' > "$USER_SKILLS_DIR/SKILL.md"
@@ -309,7 +309,7 @@ for dir in \
309
309
  "$CLAUDE_DEST/.flow-agents" \
310
310
  "$CLAUDE_DEST/.kontourai/flow-agents" \
311
311
  "$CODEX_DEST/.codex/agents" \
312
- "$CODEX_DEST/.codex/skills" \
312
+ "$CODEX_DEST/.agents/skills" \
313
313
  "$CODEX_DEST/.flow-agents" \
314
314
  "$CODEX_DEST/.kontourai/flow-agents" \
315
315
  "$CODEX_FULL_DEST/.flow-agents" \
@@ -1069,7 +1069,7 @@ else
1069
1069
  _fail "packed npm consumer did not complete the public Builder workflow contract and lifecycle commands"
1070
1070
  fi
1071
1071
 
1072
- if node - "$CODEX_FULL_DEST/.codex/skills" "$CODEX_FULL_DEST" <<'NODE'
1072
+ if node - "$CODEX_FULL_DEST/.agents/skills" "$CODEX_FULL_DEST" <<'NODE'
1073
1073
  const fs = require('node:fs');
1074
1074
  const path = require('node:path');
1075
1075
  const [root, installRoot] = process.argv.slice(2);
@@ -1094,7 +1094,12 @@ for (const skill of [...builder, 'agentic-engineering']) {
1094
1094
  throw new Error('verify-work must ship criterion-backed substantive verification evidence guidance');
1095
1095
  }
1096
1096
  const refs = [...text.matchAll(/\b(?:context|docs|kits)\/[A-Za-z0-9_./-]+\.(?:md|json)\b/g)].map((match) => match[0]);
1097
- for (const ref of refs) if (!fs.existsSync(path.join(installRoot, ref))) throw new Error(`${skill} has unresolved installed resource ${ref}`);
1097
+ for (const ref of refs) {
1098
+ const skillLocal = path.join(path.dirname(file), ref);
1099
+ if (!fs.existsSync(skillLocal) && !fs.existsSync(path.join(installRoot, ref))) {
1100
+ throw new Error(`${skill} has unresolved installed resource ${ref}`);
1101
+ }
1102
+ }
1098
1103
  }
1099
1104
  }
1100
1105
  NODE
@@ -1110,6 +1115,105 @@ else
1110
1115
  _fail "Codex full install removed unknown user files"
1111
1116
  fi
1112
1117
 
1118
+ echo ""
1119
+ echo "--- Dedicated Codex Home Universal Skills ---"
1120
+ GLOBAL_HOME="$TMPDIR_EVAL/global-home"
1121
+ GLOBAL_CODEX="$TMPDIR_EVAL/global-codex"
1122
+ GLOBAL_SKILLS="$TMPDIR_EVAL/global-agents/skills"
1123
+ mkdir -p "$GLOBAL_SKILLS/user-skill"
1124
+ printf '# user skill\n' > "$GLOBAL_SKILLS/user-skill/SKILL.md"
1125
+ GLOBAL_OUTPUT="$TMPDIR_EVAL/global-install.out"
1126
+ if HOME="$GLOBAL_HOME" CODEX_REAL_HOME="$GLOBAL_CODEX" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$GLOBAL_CODEX" --skills-dir "$GLOBAL_SKILLS" >"$GLOBAL_OUTPUT"; then
1127
+ _pass "dedicated Codex installer accepts independent hermetic runtime and skill roots"
1128
+ else
1129
+ _fail "dedicated Codex installer failed with independent hermetic roots"
1130
+ fi
1131
+ if [[ -f "$GLOBAL_SKILLS/plan-work/SKILL.md" && -f "$GLOBAL_SKILLS/plan-work/context/contracts/planning-contract.md" && -f "$GLOBAL_SKILLS/user-skill/SKILL.md" && ! -e "$GLOBAL_CODEX/skills/plan-work" ]]; then
1132
+ _pass "global install uses self-contained universal skills and preserves user skills"
1133
+ else
1134
+ _fail "global universal skill layout, resource closure, or user preservation is wrong"
1135
+ fi
1136
+ if rg -q 'Installed Flow Agents into Codex home at .+/global-codex$' "$GLOBAL_OUTPUT" && rg -q 'Installed portable skills at .+/global-agents/skills$' "$GLOBAL_OUTPUT"; then
1137
+ _pass "installer reports both runtime and portable skill destinations"
1138
+ else
1139
+ _fail "installer output does not report both destinations"
1140
+ fi
1141
+ if HOME="$GLOBAL_HOME" CODEX_REAL_HOME="$GLOBAL_CODEX" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$GLOBAL_CODEX" --skills-dir "$GLOBAL_SKILLS" >/dev/null; then
1142
+ _pass "split-root reinstall is idempotent"
1143
+ else
1144
+ _fail "split-root reinstall is not idempotent"
1145
+ fi
1146
+
1147
+ CONFLICT_CODEX="$TMPDIR_EVAL/conflict-codex"
1148
+ CONFLICT_SKILLS="$TMPDIR_EVAL/conflict-agents/skills"
1149
+ mkdir -p "$CONFLICT_SKILLS/plan-work" "$CONFLICT_CODEX/skills/plan-work" "$CONFLICT_CODEX/.flow-agents"
1150
+ printf '# user collision\n' > "$CONFLICT_SKILLS/plan-work/SKILL.md"
1151
+ cp "$ROOT_DIR/dist/codex/.agents/skills/plan-work/SKILL.md" "$CONFLICT_CODEX/skills/plan-work/SKILL.md"
1152
+ printf 'runtime sentinel\n' > "$CONFLICT_CODEX/runtime-sentinel"
1153
+ node - "$CONFLICT_CODEX" <<'NODE'
1154
+ const fs=require('node:fs'), path=require('node:path'), crypto=require('node:crypto'); const root=process.argv[2];
1155
+ const rel='skills' + '/plan-work/SKILL.md'; const sha256=crypto.createHash('sha256').update(fs.readFileSync(path.join(root,rel))).digest('hex');
1156
+ fs.writeFileSync(path.join(root,'.flow-agents/codex-install-manifest.json'), JSON.stringify({schema_version:'1.0',files:[{path:rel,sha256}]},null,2)+'\n');
1157
+ NODE
1158
+ CONFLICT_RUNTIME_BEFORE="$(shasum -a 256 "$CONFLICT_CODEX/skills/plan-work/SKILL.md" "$CONFLICT_CODEX/.flow-agents/codex-install-manifest.json" "$CONFLICT_CODEX/runtime-sentinel")"
1159
+ if HOME="$GLOBAL_HOME" CODEX_REAL_HOME="$CONFLICT_CODEX" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CONFLICT_CODEX" --skills-dir "$CONFLICT_SKILLS" >/dev/null 2>&1; then
1160
+ _fail "global installer overwrote a user-owned skill collision"
1161
+ else
1162
+ CONFLICT_RUNTIME_AFTER="$(shasum -a 256 "$CONFLICT_CODEX/skills/plan-work/SKILL.md" "$CONFLICT_CODEX/.flow-agents/codex-install-manifest.json" "$CONFLICT_CODEX/runtime-sentinel")"
1163
+ if [[ "$(cat "$CONFLICT_SKILLS/plan-work/SKILL.md")" == "# user collision" && "$CONFLICT_RUNTIME_BEFORE" == "$CONFLICT_RUNTIME_AFTER" && ! -e "$CONFLICT_SKILLS/.flow-agents" ]]; then
1164
+ _pass "cross-root collision preflight preserves universal content, legacy skill, runtime sentinel, and manifests"
1165
+ else
1166
+ _fail "cross-root collision failure partially mutated an install root"
1167
+ fi
1168
+ fi
1169
+
1170
+ SYMLINK_TARGET="$TMPDIR_EVAL/symlink-target"
1171
+ SYMLINK_PARENT="$TMPDIR_EVAL/symlink-parent"
1172
+ mkdir -p "$SYMLINK_TARGET" "$SYMLINK_PARENT"
1173
+ ln -s "$SYMLINK_TARGET" "$SYMLINK_PARENT/catalog"
1174
+ if HOME="$GLOBAL_HOME" CODEX_REAL_HOME="$TMPDIR_EVAL/symlink-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$TMPDIR_EVAL/symlink-codex" --skills-dir "$SYMLINK_PARENT/catalog/skills" >/dev/null 2>&1; then
1175
+ _fail "global installer followed a symlinked skill destination component"
1176
+ else
1177
+ [[ ! -e "$SYMLINK_TARGET/skills" ]] && _pass "global installer refuses symlinked skill destination components without mutation" || _fail "symlink rejection mutated its target"
1178
+ fi
1179
+
1180
+ SOURCE_ANCESTOR="$(dirname "$ROOT_DIR")"
1181
+ SOURCE_DESCENDANT="$ROOT_DIR/.fa-550-forbidden-skills"
1182
+ for unsafe in / "$SOURCE_ANCESTOR" "$SOURCE_DESCENDANT"; do
1183
+ rm -rf "$SOURCE_DESCENDANT"
1184
+ if HOME="$GLOBAL_HOME" CODEX_REAL_HOME="$TMPDIR_EVAL/unsafe-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$TMPDIR_EVAL/unsafe-codex" --skills-dir "$unsafe" >/dev/null 2>&1; then
1185
+ _fail "global installer accepted unsafe skill namespace root: $unsafe"
1186
+ elif [[ ! -e "$SOURCE_DESCENDANT" && ! -e "$TMPDIR_EVAL/unsafe-codex" ]]; then
1187
+ _pass "unsafe root/overlap rejection occurs before destination mutation: $unsafe"
1188
+ else
1189
+ _fail "unsafe root/overlap rejection left destination mutations: $unsafe"
1190
+ fi
1191
+ done
1192
+
1193
+ MIGRATE_CODEX="$TMPDIR_EVAL/migrate-codex"
1194
+ MIGRATE_SKILLS="$TMPDIR_EVAL/migrate-agents/skills"
1195
+ LEGACY_SKILLS_SEGMENT="skills"
1196
+ LEGACY_USER_DIR="$MIGRATE_CODEX/$LEGACY_SKILLS_SEGMENT/user-legacy"
1197
+ mkdir -p "$MIGRATE_CODEX/skills/plan-work" "$LEGACY_USER_DIR" "$MIGRATE_CODEX/.flow-agents"
1198
+ cp "$ROOT_DIR/dist/codex/.agents/skills/plan-work/SKILL.md" "$MIGRATE_CODEX/skills/plan-work/SKILL.md"
1199
+ printf '# modified legacy user content\n' > "$LEGACY_USER_DIR/SKILL.md"
1200
+ node - "$MIGRATE_CODEX" <<'NODE'
1201
+ const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto');
1202
+ const root = process.argv[2];
1203
+ const rels = ['skills' + '/plan-work/SKILL.md', 'skills' + '/user-legacy/SKILL.md'];
1204
+ const files = rels.map(p => ({path:p, sha256:crypto.createHash('sha256').update(fs.readFileSync(path.join(root,p))).digest('hex')}));
1205
+ fs.writeFileSync(path.join(root,'.flow-agents/codex-install-manifest.json'), JSON.stringify({schema_version:'1.0',files},null,2)+'\n');
1206
+ fs.appendFileSync(path.join(root,'skills' + '/user-legacy/SKILL.md'), 'kept modification\n');
1207
+ NODE
1208
+ if HOME="$GLOBAL_HOME" CODEX_REAL_HOME="$MIGRATE_CODEX" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$MIGRATE_CODEX" --skills-dir "$MIGRATE_SKILLS" >/dev/null 2>"$TMPDIR_EVAL/migrate.err" \
1209
+ && [[ ! -e "$MIGRATE_CODEX/skills/plan-work/SKILL.md" ]] \
1210
+ && rg -q 'kept modification' "$LEGACY_USER_DIR/SKILL.md" \
1211
+ && [[ -f "$MIGRATE_SKILLS/plan-work/SKILL.md" ]]; then
1212
+ _pass "legacy migration removes unchanged owned skills and preserves modified legacy content"
1213
+ else
1214
+ _fail "legacy Flow Agents skill migration was destructive or incomplete"
1215
+ fi
1216
+
1113
1217
  OPENCODE_AGENTS_DIR="$OPENCODE_FULL_DEST/.opencode/agents"
1114
1218
  if [[ -f "$OPENCODE_AGENTS_DIR/tool-planner.md" && -f "$OPENCODE_AGENTS_DIR/dev.md" ]]; then
1115
1219
  _pass "opencode full install ships the complete agent base"
@@ -164,7 +164,7 @@ OPENCODE_UPGRADE="$TMPDIR_EVAL/upgrade-opencode"
164
164
  # Touch a marker into skill files in the COPIES (not dist/ originals)
165
165
  UPGRADE_MARKER="# flow-agents-upgrade-test-marker"
166
166
  CLAUDE_SKILL_FILE="$CLAUDE_BUNDLE_COPY/.claude/skills/plan-work/SKILL.md"
167
- CODEX_SKILL_FILE="$CODEX_BUNDLE_COPY/.codex/skills/plan-work/SKILL.md"
167
+ CODEX_SKILL_FILE="$CODEX_BUNDLE_COPY/.agents/skills/plan-work/SKILL.md"
168
168
  OPENCODE_SKILL_FILE="$OPENCODE_BUNDLE_COPY/.opencode/skills/plan-work/SKILL.md"
169
169
 
170
170
  if [[ -f "$CLAUDE_SKILL_FILE" ]]; then
@@ -191,12 +191,10 @@ else
191
191
  _fail "claude-code upgrade: skill change did not propagate to workspace"
192
192
  fi
193
193
 
194
- if [[ -f "$CODEX_SKILL_FILE" ]] && grep -qF "$UPGRADE_MARKER" "$CODEX_UPGRADE/.codex/skills/plan-work/SKILL.md" 2>/dev/null; then
194
+ if [[ -f "$CODEX_SKILL_FILE" ]] && grep -qF "$UPGRADE_MARKER" "$CODEX_UPGRADE/.agents/skills/plan-work/SKILL.md" 2>/dev/null; then
195
195
  _pass "codex upgrade: modified skill file propagated to workspace"
196
- elif [[ ! -f "$CODEX_SKILL_FILE" ]]; then
197
- _pass "codex upgrade: skill file not in bundle (skipped)"
198
196
  else
199
- _fail "codex upgrade: skill change did not propagate to workspace"
197
+ _fail "codex upgrade: required universal skill missing or change did not propagate"
200
198
  fi
201
199
 
202
200
  if [[ -f "$OPENCODE_SKILL_FILE" ]] && grep -qF "$UPGRADE_MARKER" "$OPENCODE_UPGRADE/.opencode/skills/plan-work/SKILL.md" 2>/dev/null; then
@@ -246,7 +244,7 @@ echo '{"custom":"data"}' > "$OPENCODE_USER/.kontourai/flow-agents/my-session/sta
246
244
 
247
245
  # Modify an installed skill file to simulate user edits
248
246
  CLAUDE_INSTALLED_SKILL="$CLAUDE_USER/.claude/skills/plan-work/SKILL.md"
249
- CODEX_INSTALLED_SKILL="$CODEX_USER/.codex/skills/plan-work/SKILL.md"
247
+ CODEX_INSTALLED_SKILL="$CODEX_USER/.agents/skills/plan-work/SKILL.md"
250
248
  OPENCODE_INSTALLED_SKILL="$OPENCODE_USER/.opencode/skills/plan-work/SKILL.md"
251
249
 
252
250
  USER_EDIT_MARKER="# USER EDIT - should be overwritten by re-install"
@@ -22,6 +22,10 @@ set -euo pipefail
22
22
 
23
23
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
24
24
  TMPDIR_EVAL="$(mktemp -d /tmp/install-merge.XXXXXX)"
25
+ export HOME="$TMPDIR_EVAL/hermetic-home"
26
+ export FLOW_AGENTS_SKILLS_DIR="$TMPDIR_EVAL/hermetic-universal/skills"
27
+ mkdir -p "$HOME/.agents" "$FLOW_AGENTS_SKILLS_DIR"
28
+ printf 'installer tests must not replace this file\n' > "$HOME/.agents/ambient-sentinel"
25
29
  pass=0
26
30
  fail=0
27
31
 
@@ -1138,7 +1142,8 @@ echo ""
1138
1142
  echo "--- CH4: codex-home preserves user config, profiles, hooks, local kit state, and user skills ---"
1139
1143
 
1140
1144
  CH4_DEST="$TMPDIR_EVAL/codex-home-ch4"
1141
- mkdir -p "$CH4_DEST/kits/local/repositories/user-kit" "$CH4_DEST/sk""ills/user-owned-skill" "$CH4_DEST/ag""ents"
1145
+ CH4_SKILLS="$TMPDIR_EVAL/codex-home-ch4-universal-skills"
1146
+ mkdir -p "$CH4_DEST/kits/local/repositories/user-kit" "$CH4_SKILLS/user-owned-skill" "$CH4_DEST/ag""ents"
1142
1147
 
1143
1148
  CH4_GENERIC_DIRS=(agent-cards build context docs evals integrations packaging powers prompts schemas scripts kits)
1144
1149
  for CH4_DIR in "${CH4_GENERIC_DIRS[@]}"; do
@@ -1197,14 +1202,14 @@ JSON
1197
1202
  cat > "$CH4_DEST/kits/local/repositories/user-kit/kit.json" << 'JSON'
1198
1203
  {"id":"user-kit","name":"User Kit"}
1199
1204
  JSON
1200
- cat > "$CH4_DEST/sk""ills/user-owned-skill/SKILL.md" << 'EOF_SKILL'
1205
+ cat > "$CH4_SKILLS/user-owned-skill/SKILL.md" << 'EOF_SKILL'
1201
1206
  # User Owned Skill
1202
1207
  EOF_SKILL
1203
1208
  cat > "$CH4_DEST/ag""ents/user-owned-agent.toml" << 'EOF_AGENT'
1204
1209
  name = "user-owned-agent"
1205
1210
  EOF_AGENT
1206
1211
 
1207
- CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH4_DEST" >/dev/null 2>&1
1212
+ CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" FLOW_AGENTS_SKILLS_DIR="$CH4_SKILLS" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH4_DEST" >/dev/null 2>&1
1208
1213
 
1209
1214
  if grep -q 'user-model' "$CH4_DEST/config.toml" \
1210
1215
  && grep -q 'user-profile-model' "$CH4_DEST/custom.config.toml" \
@@ -1243,10 +1248,10 @@ else
1243
1248
  _fail "CH4: kits/local registry or repository was removed"
1244
1249
  fi
1245
1250
 
1246
- if [[ -f "$CH4_DEST/sk""ills/search-first/SKILL.md" && -f "$CH4_DEST/sk""ills/plan-work/SKILL.md" && -f "$CH4_DEST/sk""ills/user-owned-skill/SKILL.md" ]]; then
1247
- _pass "CH4: root, Builder Kit workflow, and user-owned skills coexist in flattened Codex skills"
1251
+ if [[ -f "$CH4_SKILLS/search-first/SKILL.md" && -f "$CH4_SKILLS/plan-work/SKILL.md" && -f "$CH4_SKILLS/user-owned-skill/SKILL.md" ]]; then
1252
+ _pass "CH4: root, Builder Kit workflow, and user-owned skills coexist in universal Codex skills"
1248
1253
  else
1249
- _fail "CH4: root skill, Builder Kit workflow skill, or user-owned skill missing from flattened Codex skills"
1254
+ _fail "CH4: root skill, Builder Kit workflow skill, or user-owned skill missing from universal Codex skills"
1250
1255
  fi
1251
1256
 
1252
1257
  if [[ -f "$CH4_DEST/scripts/install-merge.js" && -f "$CH4_DEST/build/src/cli/kit.js" && -f "$CH4_DEST/ag""ents/tool-worker.toml" && -f "$CH4_DEST/ag""ents/user-owned-agent.toml" && -f "$CH4_DEST/hooks.json" ]] \
@@ -1272,7 +1277,7 @@ else
1272
1277
  _fail "CH4: installed scripts/kit.js could not run with installed build bundle"
1273
1278
  fi
1274
1279
 
1275
- CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH4_DEST" >/dev/null 2>&1
1280
+ CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" FLOW_AGENTS_SKILLS_DIR="$CH4_SKILLS" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH4_DEST" >/dev/null 2>&1
1276
1281
  CH4_GENERIC_PRESERVED=1
1277
1282
  for CH4_DIR in "${CH4_GENERIC_DIRS[@]}"; do
1278
1283
  [[ "$(cat "$CH4_DEST/$CH4_DIR/user-owned.txt" 2>/dev/null)" == "user-owned-$CH4_DIR" ]] || CH4_GENERIC_PRESERVED=0
@@ -1288,7 +1293,7 @@ echo ""
1288
1293
  # ─── codex-home: CH5: destination symlink containment ───────────────────────
1289
1294
  echo "--- CH5: codex-home refuses managed destination symlinks ---"
1290
1295
 
1291
- for CH5_REL in scripts kits sk""ills hooks.json; do
1296
+ for CH5_REL in scripts kits hooks.json; do
1292
1297
  CH5_DEST="$TMPDIR_EVAL/codex-home-ch5-${CH5_REL//\//-}"
1293
1298
  CH5_OUTSIDE="$TMPDIR_EVAL/codex-home-ch5-outside-${CH5_REL//\//-}"
1294
1299
  mkdir -p "$CH5_DEST" "$CH5_OUTSIDE"
@@ -1680,6 +1685,11 @@ echo ""
1680
1685
 
1681
1686
  echo ""
1682
1687
  echo "==========================="
1688
+ if [[ "$(cat "$HOME/.agents/ambient-sentinel" 2>/dev/null)" == "installer tests must not replace this file" ]]; then
1689
+ _pass "dedicated Codex installer calls remain inside hermetic universal roots"
1690
+ else
1691
+ _fail "dedicated Codex installer calls mutated the isolated default-home sentinel"
1692
+ fi
1683
1693
  total=$((pass + fail))
1684
1694
  echo "Results: ${pass}/${total} passed, ${fail} failed"
1685
1695
  [[ "$fail" -gt 0 ]] && exit 1
@@ -160,15 +160,21 @@ pass "status is canonical and byte-read-only"
160
160
 
161
161
  FLOW_MANIFEST="$CONSUMER/.kontourai/flow/runs/acme-widgets-101/evidence/manifest.json"
162
162
  BEFORE_EVIDENCE="$(node -p "JSON.parse(require('fs').readFileSync('$FLOW_MANIFEST')).evidence.length")"
163
- run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation pickup-probe-readiness --status not_verified --summary "Consumer fixture intentionally leaves this claim unverified." --json >/dev/null
163
+ PARTIAL_EVIDENCE="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation pickup-probe-readiness --status not_verified --summary "Consumer fixture intentionally leaves this claim unverified." --json)"
164
164
  AFTER_EVIDENCE="$(node -p "JSON.parse(require('fs').readFileSync('$FLOW_MANIFEST')).evidence.length")"
165
- [[ "$AFTER_EVIDENCE" -eq $((BEFORE_EVIDENCE + 1)) ]] || fail "evidence invocation did not attach exactly once"
166
- pass "evidence records and synchronizes exactly once"
165
+ [[ "$AFTER_EVIDENCE" -eq "$BEFORE_EVIDENCE" ]] || fail "partial evidence changed the canonical manifest"
166
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.attached!==false||r.awaiting_evidence!==true||r.current_step!=="design-probe")process.exit(1)' "$PARTIAL_EVIDENCE" || fail "partial evidence did not report an explicit awaiting-evidence result"
167
+ pass "partial evidence records locally without evaluation or canonical attachment"
167
168
 
168
169
  PULL_REPORT="$RELEASE_SESSION/$(basename "$RELEASE_SESSION")--pull-work.md"
169
170
  PULL_REPORT_REF="{\"kind\":\"artifact\",\"file\":\"$PULL_REPORT\",\"summary\":\"Concrete selected-work and probe report.\"}"
170
- run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation pickup-probe-readiness --status pass --summary "Complete the consumer readiness expectation after exercising NOT_VERIFIED." --evidence-ref-json "$PULL_REPORT_REF" --json >/dev/null
171
- run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation probe-decisions-or-accepted-gaps --status pass --summary "Complete the consumer probe gate." --evidence-ref-json "$PULL_REPORT_REF" --json >/dev/null
171
+ READINESS_PARTIAL="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation pickup-probe-readiness --status pass --summary "Complete the consumer readiness expectation after exercising NOT_VERIFIED." --evidence-ref-json "$PULL_REPORT_REF" --json)"
172
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.attached!==false||r.awaiting_evidence!==true||r.current_step!=="design-probe")process.exit(1)' "$READINESS_PARTIAL" || fail "first passing member of a multi-expectation gate did not remain pending"
173
+ PROBE_COMPLETE="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation probe-decisions-or-accepted-gaps --status pass --summary "Complete the consumer probe gate." --evidence-ref-json "$PULL_REPORT_REF" --json)"
174
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.attached!==true||r.awaiting_evidence!==false||r.current_step!=="plan")process.exit(1)' "$PROBE_COMPLETE" || fail "complete multi-expectation gate did not attach and advance exactly once"
175
+ AFTER_COMPLETE_EVIDENCE="$(node -p "JSON.parse(require('fs').readFileSync('$FLOW_MANIFEST')).evidence.length")"
176
+ [[ "$AFTER_COMPLETE_EVIDENCE" -eq $((BEFORE_EVIDENCE + 1)) ]] || fail "complete multi-expectation gate did not attach exactly once"
177
+ pass "multi-expectation evidence attaches atomically only when complete"
172
178
  seed_pull_work acme/widgets#104
173
179
  run_candidate_as poison-pointer start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#104 --assignment-provider local-file --summary "Poison global pointer fixture" >/dev/null
174
180
  node - "$ARTIFACT_ROOT" "$(basename "$RELEASE_SESSION")" <<'NODE'
@@ -10,7 +10,7 @@ pass=0
10
10
  fail=0
11
11
 
12
12
  cleanup() {
13
- rm -rf "$REPRO_FIRST_DIR" "$REPRO_SECOND_DIR" "$ROOT_DIR/kits/zzz-collision-eval-probe"
13
+ rm -rf "$REPRO_FIRST_DIR" "$REPRO_SECOND_DIR" "$ROOT_DIR/kits/zzz-collision-eval-probe" "${MISSING_SKILL_DIR:-}"
14
14
  }
15
15
  trap cleanup EXIT
16
16
 
@@ -65,6 +65,49 @@ for dir in "$DIST_DIR/kiro" "$DIST_DIR/claude-code" "$DIST_DIR/codex" "$DIST_DIR
65
65
  fi
66
66
  done
67
67
 
68
+ if [[ -d "$DIST_DIR/codex/.agents/skills/plan-work" && ! -e "$DIST_DIR/codex/.codex/skills" ]]; then
69
+ _pass "Codex exports portable skills only in the universal .agents catalog"
70
+ else
71
+ _fail "Codex portable skills are not rooted exclusively at .agents/skills"
72
+ fi
73
+
74
+ if node - "$DIST_DIR/codex/.agents/skills" <<'NODE'
75
+ const fs = require("node:fs");
76
+ const path = require("node:path");
77
+ const root = process.argv[2];
78
+ const pattern = /(?:^|[`("'\s])(context\/contracts\/[A-Za-z0-9._/-]+\.md)(?=$|[`),"'\s])/gm;
79
+ for (const name of fs.readdirSync(root)) {
80
+ const skill = path.join(root, name);
81
+ const file = path.join(skill, "SKILL.md");
82
+ if (!fs.existsSync(file)) continue;
83
+ for (const match of fs.readFileSync(file, "utf8").matchAll(pattern)) {
84
+ const resolved = path.resolve(skill, match[1]);
85
+ if (!resolved.startsWith(`${path.resolve(skill)}${path.sep}`) || !fs.existsSync(resolved)) throw new Error(`${name}: ${match[1]}`);
86
+ }
87
+ }
88
+ NODE
89
+ then
90
+ _pass "Codex skill-local instruction resources resolve inside each package"
91
+ else
92
+ _fail "Codex skill-local instruction resource closure failed"
93
+ fi
94
+
95
+ MISSING_SKILL_DIR="$ROOT_DIR/${SKILLS_SEGMENT:-skills}/zzz-missing-resource-probe"
96
+ MISSING_CONTRACT="${CONTEXT_SEGMENT:-context}/contracts/definitely-missing.md"
97
+ mkdir -p "$MISSING_SKILL_DIR"
98
+ printf '%s\n' '# Probe' '' "Read \`$MISSING_CONTRACT\`." > "$MISSING_SKILL_DIR/SKILL.md"
99
+ if (cd "$ROOT_DIR" && npm run build:bundles >/tmp/missing-skill-resource.stdout 2>/tmp/missing-skill-resource.stderr); then
100
+ _fail "bundle generation accepted a missing skill instruction resource"
101
+ else
102
+ if rg -q "skill 'zzz-missing-resource-probe': missing local resource '$MISSING_CONTRACT'" /tmp/missing-skill-resource.stderr; then
103
+ _pass "bundle generation rejects missing skill resources with actionable diagnostics"
104
+ else
105
+ _fail "missing skill resource failure did not name its skill and path"
106
+ fi
107
+ fi
108
+ rm -rf "$MISSING_SKILL_DIR"
109
+ (cd "$ROOT_DIR" && npm run build:bundles >/dev/null)
110
+
68
111
  source_agents=$(find "$ROOT_DIR/agents" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')
69
112
  codex_excluded_agents=$(node - "$ROOT_DIR/packaging/manifest.json" <<'NODE'
70
113
  const fs = require("node:fs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
5
5
  "keywords": [
6
6
  "agents",
@@ -19,7 +19,7 @@ For the full source/generated/runtime inventory, see [Repository Structure](../d
19
19
 
20
20
  - `dist/kiro/` keeps native Kiro JSON agents and rewrites path-bound config through the install token.
21
21
  - `dist/claude-code/` exports `.claude/agents/*.md` and `.claude/skills/*/SKILL.md`.
22
- - `dist/codex/` exports `.codex/agents/*.toml`, `.codex/skills/*/SKILL.md`, and generated profile config for operating intents such as `builder` and `personal`.
22
+ - `dist/codex/` exports Codex-only agents and profiles beneath `.codex/`, and portable, self-contained skills beneath the universal `.agents/skills/` catalog.
23
23
 
24
24
  All targets also receive shared canonical directories where supported: `context/`, `powers/`, `prompts/`, `scripts/`, and `evals/`.
25
25
 
package/scripts/README.md CHANGED
@@ -113,7 +113,7 @@ prompting. Add `--allow-network` to probe a non-local HTTPS Console endpoint.
113
113
 
114
114
  ## Install And Repo Utilities
115
115
 
116
- - `install-codex-home.sh`: installs the generated Codex bundle into `CODEX_HOME`, or `~/.codex` when `CODEX_HOME` is unset. A positional destination argument is the explicit override for isolated homes and tests.
116
+ - `install-codex-home.sh`: installs Codex runtime assets into `CODEX_HOME` (or `~/.codex`) and portable skills into `$HOME/.agents/skills`. Use `--skills-dir PATH` or `FLOW_AGENTS_SKILLS_DIR` to select a hermetic or advanced-user skill catalog independently of the positional runtime destination. The installer reports both resolved roots, preserves user-owned files, migrates only unchanged Flow Agents-owned legacy skills, and refuses symlink destinations rather than creating a compatibility symlink.
117
117
  - `setup-repo-hooks.sh`: configures this clone's Git hook path.
118
118
  - `check-content-boundary.cjs`, `detect-tools.sh`, `discover-agents.sh`, `git-status.sh`: repo-local helper commands.
119
119
  - `context-budget/` and `statusline/`: specialized support tooling copied into bundles where needed.
@@ -16,14 +16,21 @@ Options:
16
16
  --console-token-file PATH
17
17
  Read Console telemetry bearer token from a file.
18
18
  --console-tenant ID Persist Console tenant identifier.
19
+ --skills-dir PATH Install portable skills here (default: $HOME/.agents/skills).
19
20
  EOF
20
21
  }
21
22
 
22
23
  DEST="${CODEX_HOME:-$HOME/.codex}"
24
+ SKILLS_DIR="${FLOW_AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
23
25
  DEST_SET=0
24
26
  CONSOLE_CONFIG_ARGS=()
25
27
  while [[ $# -gt 0 ]]; do
26
28
  case "$1" in
29
+ --skills-dir)
30
+ [[ $# -ge 2 ]] || { echo "install-codex-home.sh: $1 requires a value" >&2; exit 2; }
31
+ SKILLS_DIR="$2"
32
+ shift 2
33
+ ;;
27
34
  --telemetry-sink|--telemetry-sinks|--console-url|--console-endpoint|--console-endpoint-url|--console-token-file|--console-tenant|--console-tenant-id)
28
35
  [[ $# -ge 2 ]] || { echo "install-codex-home.sh: $1 requires a value" >&2; exit 2; }
29
36
  CONSOLE_CONFIG_ARGS+=("$1" "$2")
@@ -69,9 +76,26 @@ NODE
69
76
  }
70
77
 
71
78
  [[ ! -L "$DEST" ]] || { echo "install-codex-home.sh: refusing symlink destination root: $DEST" >&2; exit 1; }
79
+ [[ ! -L "$SKILLS_DIR" ]] || { echo "install-codex-home.sh: refusing symlink skills destination root: $SKILLS_DIR" >&2; exit 1; }
72
80
  ROOT_REAL="$(canonicalize_path "$ROOT_DIR")"
73
81
  BUNDLE_SOURCE_REAL="$(canonicalize_path "$ROOT_DIR/dist/codex")"
74
82
  DEST_REAL="$(canonicalize_path "$DEST")"
83
+ SKILLS_DIR_REAL="$(canonicalize_path "$SKILLS_DIR")"
84
+ if ! node - "$SKILLS_DIR" <<'NODE'
85
+ const fs = require("node:fs"); const path = require("node:path");
86
+ const absolute = path.resolve(process.argv[2]);
87
+ let current = path.parse(absolute).root;
88
+ for (const part of absolute.slice(current.length).split(path.sep).filter(Boolean)) {
89
+ current = path.join(current, part);
90
+ // macOS exposes /tmp as the system-managed /private/tmp symlink. Treat that
91
+ // mount alias as trusted while rejecting caller-controlled components.
92
+ if (current !== "/tmp" && fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) process.exit(1);
93
+ }
94
+ NODE
95
+ then
96
+ echo "install-codex-home.sh: refusing symlink component in skills destination: $SKILLS_DIR" >&2
97
+ exit 1
98
+ fi
75
99
  REAL_CODEX_HOME_REAL="$(canonicalize_path "$REAL_CODEX_HOME")"
76
100
  case "$DEST_REAL/" in
77
101
  "$ROOT_REAL/"*|"$BUNDLE_SOURCE_REAL/"*)
@@ -85,6 +109,17 @@ case "$ROOT_REAL/" in
85
109
  exit 1
86
110
  ;;
87
111
  esac
112
+ case "$SKILLS_DIR_REAL/" in
113
+ "$ROOT_REAL/"*|"$BUNDLE_SOURCE_REAL/"*) echo "install-codex-home.sh: skills destination overlaps Flow Agents source: $SKILLS_DIR_REAL" >&2; exit 1 ;;
114
+ esac
115
+ case "$ROOT_REAL/" in
116
+ "$SKILLS_DIR_REAL/"*) echo "install-codex-home.sh: Flow Agents source overlaps skills destination: $SKILLS_DIR_REAL" >&2; exit 1 ;;
117
+ esac
118
+ case "$BUNDLE_SOURCE_REAL/" in
119
+ "$SKILLS_DIR_REAL/"*) echo "install-codex-home.sh: generated bundle source overlaps skills destination: $SKILLS_DIR_REAL" >&2; exit 1 ;;
120
+ esac
121
+ SKILLS_PATH_ROOT="$(node -e 'process.stdout.write(require("node:path").parse(process.argv[1]).root)' "$SKILLS_DIR_REAL")"
122
+ [[ "$SKILLS_DIR_REAL" != "$SKILLS_PATH_ROOT" ]] || { echo "install-codex-home.sh: refusing filesystem-root skills destination: $SKILLS_DIR_REAL" >&2; exit 1; }
88
123
 
89
124
  if command -v npm >/dev/null 2>&1; then
90
125
  (cd "$ROOT_DIR" && FLOW_AGENTS_EXPORT_DIAGNOSTICS=0 npm run build:bundles --silent >/dev/null)
@@ -93,10 +128,6 @@ else
93
128
  exit 1
94
129
  fi
95
130
 
96
- mkdir -p "$DEST"
97
- DEST="$(cd "$DEST" && pwd -P)"
98
- DEST_REAL="$DEST"
99
-
100
131
  assert_safe_dest_path() {
101
132
  local rel="$1"
102
133
  local current="$DEST"
@@ -122,22 +153,17 @@ assert_safe_dest_path() {
122
153
  esac
123
154
  }
124
155
 
125
- # Stash the user's existing hooks.json (if any), so the merge step below can
126
- # preserve user hooks across installs while replacing Flow Agents-managed groups.
127
156
  FA_USER_HOOKS_STASH=""
128
- assert_safe_dest_path "hooks.json"
129
- if [[ -f "$DEST/hooks.json" ]]; then
130
- FA_USER_HOOKS_STASH="$(mktemp /tmp/fa-user-hooks.XXXXXX.json)"
131
- cp "$DEST/hooks.json" "$FA_USER_HOOKS_STASH"
132
- fi
133
157
 
134
158
  # A real Codex home can contain user-owned files in every shared directory.
135
159
  # Prepare an exact Flow Agents overlay, then synchronize only files recorded in
136
160
  # the ownership manifest. The synchronizer never uses directory-wide --delete:
137
161
  # it removes only unchanged files from its prior manifest and refuses collisions.
138
162
  FA_OWNED_OVERLAY="$(mktemp -d /tmp/fa-codex-overlay.XXXXXX)"
163
+ FA_SKILLS_OVERLAY="$(mktemp -d /tmp/fa-codex-skills-overlay.XXXXXX)"
139
164
  cleanup_install_temps() {
140
165
  rm -rf "$FA_OWNED_OVERLAY"
166
+ rm -rf "$FA_SKILLS_OVERLAY"
141
167
  [[ -z "${FA_USER_HOOKS_STASH:-}" ]] || rm -f "$FA_USER_HOOKS_STASH"
142
168
  }
143
169
  trap cleanup_install_temps EXIT
@@ -157,22 +183,14 @@ for managed_dir in \
157
183
  scripts
158
184
  do
159
185
  if [[ -d "$ROOT_DIR/dist/codex/$managed_dir" ]]; then
160
- assert_safe_dest_path "$managed_dir"
161
186
  mkdir -p "$FA_OWNED_OVERLAY/$managed_dir"
162
187
  rsync -a "$ROOT_DIR/dist/codex/$managed_dir/" "$FA_OWNED_OVERLAY/$managed_dir/"
163
188
  fi
164
189
  done
165
190
 
166
- # Skills are user-extensible in a real Codex home. Merge both bundle skill
167
- # layers into the flattened destination without deleting user-owned skills.
168
- if [[ -d "$ROOT_DIR/dist/codex/skills" ]]; then
169
- mkdir -p "$FA_OWNED_OVERLAY/skills"
170
- rsync -a "$ROOT_DIR/dist/codex/skills/" "$FA_OWNED_OVERLAY/skills/"
171
- fi
172
-
173
- if [[ -d "$ROOT_DIR/dist/codex/.codex/skills" ]]; then
174
- mkdir -p "$FA_OWNED_OVERLAY/skills"
175
- rsync -a "$ROOT_DIR/dist/codex/.codex/skills/" "$FA_OWNED_OVERLAY/skills/"
191
+ # Portable skills use Codex's universal catalog, independently of CODEX_HOME.
192
+ if [[ -d "$ROOT_DIR/dist/codex/.agents/skills" ]]; then
193
+ rsync -a "$ROOT_DIR/dist/codex/.agents/skills/" "$FA_SKILLS_OVERLAY/"
176
194
  fi
177
195
 
178
196
  if [[ -d "$ROOT_DIR/dist/codex/kits" ]]; then
@@ -193,10 +211,31 @@ for bundle_file in README.md console.telemetry.json install.sh; do
193
211
  fi
194
212
  done
195
213
 
196
- assert_safe_dest_path ".flow-agents/codex-install-manifest.json"
214
+ # Check both destinations with the exact synchronizer before either can mutate.
215
+ node "$ROOT_DIR/scripts/install-owned-files.js" \
216
+ --check "$FA_SKILLS_OVERLAY" "$SKILLS_DIR_REAL" ".flow-agents/codex-universal-skills-install-manifest.json"
217
+ node "$ROOT_DIR/scripts/install-owned-files.js" \
218
+ --check "$FA_OWNED_OVERLAY" "$DEST_REAL" ".flow-agents/codex-install-manifest.json"
219
+
220
+ mkdir -p "$SKILLS_DIR"
221
+ SKILLS_DIR="$(cd "$SKILLS_DIR" && pwd -P)"
222
+ node "$ROOT_DIR/scripts/install-owned-files.js" \
223
+ "$FA_SKILLS_OVERLAY" "$SKILLS_DIR" ".flow-agents/codex-universal-skills-install-manifest.json"
224
+
225
+ mkdir -p "$DEST"
226
+ DEST="$(cd "$DEST" && pwd -P)"
227
+ DEST_REAL="$DEST"
197
228
  node "$ROOT_DIR/scripts/install-owned-files.js" \
198
229
  "$FA_OWNED_OVERLAY" "$DEST" ".flow-agents/codex-install-manifest.json"
199
230
 
231
+ # Stash the user's existing hooks.json (if any), so the merge step below can
232
+ # preserve user hooks across installs while replacing Flow Agents-managed groups.
233
+ assert_safe_dest_path "hooks.json"
234
+ if [[ -f "$DEST/hooks.json" ]]; then
235
+ FA_USER_HOOKS_STASH="$(mktemp /tmp/fa-user-hooks.XXXXXX.json)"
236
+ cp "$DEST/hooks.json" "$FA_USER_HOOKS_STASH"
237
+ fi
238
+
200
239
  atomic_copy() {
201
240
  local source="$1"
202
241
  local rel="$2"
@@ -289,6 +328,7 @@ if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" |
289
328
  fi
290
329
 
291
330
  echo "Installed Flow Agents into Codex home at $DEST"
331
+ echo "Installed portable skills at $SKILLS_DIR"
292
332
  if [[ "${#profile_names[@]}" -gt 0 ]]; then
293
333
  echo "Profiles: ${profile_names[*]}"
294
334
  fi
@@ -9,10 +9,24 @@ function fail(message) {
9
9
  process.exit(1);
10
10
  }
11
11
 
12
- const [sourceArg, destArg, manifestArg] = process.argv.slice(2);
12
+ const args = process.argv.slice(2);
13
+ const checkOnly = args[0] === "--check";
14
+ if (checkOnly) args.shift();
15
+ const [sourceArg, destArg, manifestArg] = args;
13
16
  if (!sourceArg || !destArg || !manifestArg) fail("usage: install-owned-files.js <overlay> <destination> <manifest-relative-path>");
14
17
  const source = fs.realpathSync(sourceArg);
15
- const dest = fs.realpathSync(destArg);
18
+ function canonicalizeMissing(value) {
19
+ let current = path.resolve(value);
20
+ const missing = [];
21
+ while (!fs.existsSync(current)) {
22
+ missing.unshift(path.basename(current));
23
+ const parent = path.dirname(current);
24
+ if (parent === current) break;
25
+ current = parent;
26
+ }
27
+ return path.resolve(fs.realpathSync(current), ...missing);
28
+ }
29
+ const dest = canonicalizeMissing(destArg);
16
30
  const manifestPath = path.join(dest, manifestArg);
17
31
 
18
32
  function hashFile(file) {
@@ -132,6 +146,8 @@ for (const [rel] of previous) {
132
146
  if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) fail(`refusing to remove symlink replacing owned file: ${target}`);
133
147
  }
134
148
 
149
+ if (checkOnly) process.exit(0);
150
+
135
151
  for (const entry of incoming) {
136
152
  const target = targetFor(entry.rel);
137
153
  ensureSafeParent(target, true);