@event4u/agent-config 5.4.1 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.agent-src/commands/image/analyse.md +51 -0
  2. package/.agent-src/commands/image/create.md +53 -0
  3. package/.agent-src/commands/image/verify.md +48 -0
  4. package/.agent-src/commands/image.md +69 -0
  5. package/.agent-src/commands/knowledge/cross-repo.md +71 -0
  6. package/.agent-src/commands/knowledge.md +2 -0
  7. package/.agent-src/commands/skill/preview.md +67 -0
  8. package/.agent-src/commands/skill.md +48 -0
  9. package/.agent-src/commands/skills/discover.md +76 -0
  10. package/.agent-src/commands/skills.md +56 -0
  11. package/.agent-src/commands/video/from-song.md +351 -0
  12. package/.agent-src/commands/video.md +19 -9
  13. package/.agent-src/contexts/authority/commit-mechanics.md +8 -0
  14. package/.agent-src/rules/commit-policy.md +3 -8
  15. package/.agent-src/rules/linked-projects-onboarding-gate.md +1 -1
  16. package/.agent-src/rules/media-sync-ground-truth.md +58 -0
  17. package/.agent-src/skills/image-analyser/SKILL.md +121 -0
  18. package/.agent-src/skills/image-analyser/canon-spec.md +109 -0
  19. package/.agent-src/skills/image-analyser/evals/triggers.json +16 -0
  20. package/.agent-src/skills/image-creator/SKILL.md +117 -0
  21. package/.agent-src/skills/image-creator/evals/triggers.json +16 -0
  22. package/.agent-src/skills/song-to-script/SKILL.md +216 -0
  23. package/.claude-plugin/marketplace.json +15 -2
  24. package/CHANGELOG.md +84 -0
  25. package/CONTRIBUTING.md +6 -0
  26. package/README.md +3 -3
  27. package/config/agent-settings.template.yml +18 -0
  28. package/dist/cli/registry.js +1 -0
  29. package/dist/cli/registry.js.map +1 -1
  30. package/dist/discovery/deprecation-report.md +1 -1
  31. package/dist/discovery/discovery-manifest.json +327 -20
  32. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  33. package/dist/discovery/discovery-manifest.summary.md +4 -4
  34. package/dist/discovery/orphan-report.md +1 -1
  35. package/dist/discovery/packs.json +24 -10
  36. package/dist/discovery/trust-report.md +3 -3
  37. package/dist/discovery/workspaces.json +20 -6
  38. package/dist/mcp/registry-manifest.json +3 -3
  39. package/dist/router.json +1 -1
  40. package/dist/server/schemas/settings.js +4 -0
  41. package/dist/server/schemas/settings.js.map +1 -1
  42. package/docs/architecture.md +3 -3
  43. package/docs/catalog.md +20 -6
  44. package/docs/contracts/benchmark-report-schema.md +12 -10
  45. package/docs/contracts/command-clusters.md +5 -1
  46. package/docs/contracts/cross-repo-retrieval.md +64 -0
  47. package/docs/contracts/rule-router.md +39 -0
  48. package/docs/contracts/skill-discovery.md +80 -0
  49. package/docs/contracts/skill-dry-run.md +47 -0
  50. package/docs/contracts/value-dashboard-spec.md +7 -3
  51. package/docs/contracts/value-report-schema.md +6 -1
  52. package/docs/decisions/ADR-032-linked-projects-scope.md +7 -3
  53. package/docs/getting-started.md +2 -2
  54. package/docs/guides/cross-repo-linked-projects.md +7 -0
  55. package/docs/guides/cross-repo-retrieval.md +61 -0
  56. package/docs/guides/skill-discovery.md +71 -0
  57. package/docs/guides/skill-preview.md +71 -0
  58. package/docs/value.md +17 -17
  59. package/package.json +1 -1
  60. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  61. package/scripts/_dispatch.bash +10 -0
  62. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  63. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  64. package/scripts/_lib/bench_report.py +13 -14
  65. package/scripts/_lib/bench_telegraph_report.py +1 -2
  66. package/scripts/_lib/token_count.py +95 -0
  67. package/scripts/_lib/value_report.py +3 -3
  68. package/scripts/ai-video/adapters/higgsfield.sh +163 -6
  69. package/scripts/ai-video/adapters/openai-images.sh +92 -6
  70. package/scripts/ai-video/lib/probe-audio.sh +181 -0
  71. package/scripts/audit_auto_rules.py +22 -6
  72. package/scripts/audit_command_surface.py +6 -1
  73. package/scripts/audit_initial_context.py +210 -0
  74. package/scripts/bench_ab_diff.py +4 -11
  75. package/scripts/bench_run.py +2 -3
  76. package/scripts/bench_runner.py +2 -2
  77. package/scripts/condense.py +44 -3
  78. package/scripts/cross_repo_retrieve.py +172 -0
  79. package/scripts/inventory_meta_layers.py +288 -0
  80. package/scripts/iron_law_sha.py +14 -5
  81. package/scripts/linked_projects_list.py +91 -0
  82. package/scripts/measure_rule_budget.py +15 -0
  83. package/scripts/memory_lookup.py +53 -2
  84. package/scripts/project_thin_rules.py +168 -0
  85. package/scripts/render_value_md.py +14 -23
  86. package/scripts/schemas/command.schema.json +1 -1
  87. package/scripts/schemas/rule.schema.json +1 -1
  88. package/scripts/schemas/skill.schema.json +2 -2
  89. package/scripts/skill_discovery.py +254 -0
  90. package/scripts/skill_linter.py +8 -4
  91. package/scripts/skill_preview.py +179 -0
  92. package/scripts/trigger_coverage.py +129 -0
package/docs/value.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Wie diese Seite zu lesen ist
6
6
 
7
- **Panel A (Kostenleiter)** — von oben nach unten lesen. Jede Stufe sagt: *was sie macht*, *wie viele Input-Tokens sie pro Request hinzufügt oder spart*, *was das in € auf 1,000 Requests kostet*, und *wo wir kumulativ stehen*. Die fett gedruckte **NETTO**-Zeile am Ende ist die Antwort.
7
+ **Panel A (Token-Leiter)** — von oben nach unten lesen. Jede Stufe sagt: *was sie macht*, *wie viele Input-Tokens sie pro Request hinzufügt oder spart*, und *wo wir kumulativ stehen*. Die fett gedruckte **NETTO**-Zeile am Ende ist die Antwort. Bewusst rein in Tokens — kein €-Vergleich, da Abo-Nutzer keine Per-Request-API-Preise zahlen.
8
8
 
9
9
  **Panel B (Verhalten)** — vier reale Vergleiche, *mit* vs. *ohne* Paket. Hier liegt der nicht-Token-Wert: passende Skill-Auswahl, Stopps bei riskanten Aktionen, weniger Rückfragen, mehr abgeschlossene Aufgaben.
10
10
 
@@ -13,25 +13,25 @@
13
13
  ## Reference scale
14
14
 
15
15
  - **1,000** Requests, durchschnittlich **8,000** Input-Tokens und **600** Output-Tokens pro Request
16
- - Modell-Tier: `sonnet` · Preisstand `2026-05-14` (Quelle: `internal/bench/pricing.yaml`)
16
+ - Modell-Tier (Workload-Annahme): `sonnet`
17
17
  - Wer einen anderen Workload fährt, rechnet selbst nach — die Methodik ist offengelegt; nichts ist hardcodiert versteckt.
18
18
 
19
19
  ## Panel A — Kostenleiter (kumulativ, min → max)
20
20
 
21
21
  Liest sich von oben nach unten. Positive Δ-Werte = das Paket *kostet* Tokens (Regel-Load ist die ehrliche Up-Front-Steuer); negative Δ-Werte = das Paket *spart* Tokens.
22
22
 
23
- | Stufe | Was sie tut | Δ Tokens | Δ € (1k Req) | Kumulativ | Quelle |
24
- |---|---|---:|---:|---:|---|
25
- | **Ohne Paket / Without package** | Baseline — der nackte Request ohne Paket-Regeln. | +0 | +0.00 € | +0.00% | `n/a` · ✅ gemessen |
26
- | Mit Paket (Regeln laden) / With package (rule load) | Die immer-aktiven Regeln landen im Kontext jedes Requests. ⚠️ erst teurer | +8 895 | +24.55 € | +111.19% | `dist/router.json` · ✅ gemessen |
27
- | | _Fußnote:_ Kernel = 10 rules (31570 chars) + charter (4010 chars); tokens ≈ chars / 4. | | | | |
28
- | + condense (Regeln eindampfen) / + condense (rule shrink) | Build-Schritt schrumpft Regel-Dateien vor dem Ausliefern. | -186 | -0.51 € | +108.86% | `internal/bench/reports/telegraph-v2.json` · ✅ gemessen |
29
- | | _Fußnote:_ Aggregate across non-Thin-Root categories; Thin-Root files (AGENTS.md variants) net negative (~−4%) and are excluded from the rung — surfaced separately. | | | | |
30
- | + rtk (CLI-Output filtern) / + rtk (filter CLI output) | rtk schneidet verbose CLI-Ausgabe vor dem Modell-Input weg. | -593 | -1.64 € | +101.45% | `internal/bench/reports/rtk/latest.json` · ✅ gemessen |
31
- | + terse (Antworten knapper) / + terse (shorter replies) | Telegraph-Stil zielt auf knappere Modell-Antworten. | +56 | +0.77 € | +102.15% | `internal/bench/reports/telegraph-v1.json` · ✅ gemessen |
32
- | | _Fußnote:_ Honest: gemessener Median = -9.27% gegen 'sei knapp' — Telegraph liefert hier mehr Tokens, nicht weniger. Wir messen, wir verstecken nicht. | | | | |
23
+ | Stufe | Was sie tut | Δ Tokens | Kumulativ | Quelle |
24
+ |---|---|---:|---:|---|
25
+ | **Ohne Paket / Without package** | Baseline — der nackte Request ohne Paket-Regeln. | +0 | +0.00% | `n/a` · ✅ gemessen |
26
+ | Mit Paket (Regeln laden) / With package (rule load) | Die immer-aktiven Regeln landen im Kontext jedes Requests. ⚠️ erst teurer | +8 522 | +106.53% | `dist/router.json` · ✅ gemessen |
27
+ | | _Fußnote:_ Kernel = 10 rules (30080 chars) + charter (4010 chars); tokens ≈ chars / 4. | | | |
28
+ | + condense (Regeln eindampfen) / + condense (rule shrink) | Build-Schritt schrumpft Regel-Dateien vor dem Ausliefern. | -186 | +104.20% | `internal/bench/reports/telegraph-v2.json` · ✅ gemessen |
29
+ | | _Fußnote:_ Aggregate across non-Thin-Root categories; Thin-Root files (AGENTS.md variants) net negative (~−4%) and are excluded from the rung — surfaced separately. | | | |
30
+ | + rtk (CLI-Output filtern) / + rtk (filter CLI output) | rtk schneidet verbose CLI-Ausgabe vor dem Modell-Input weg. | -593 | +96.79% | `internal/bench/reports/rtk/latest.json` · ✅ gemessen |
31
+ | + terse (Antworten knapper) / + terse (shorter replies) | Telegraph-Stil zielt auf knappere Modell-Antworten. | +56 | +97.49% | `internal/bench/reports/telegraph-v1.json` · ✅ gemessen |
32
+ | | _Fußnote:_ Honest: gemessener Median = -9.27% gegen 'sei knapp' — Telegraph liefert hier mehr Tokens, nicht weniger. Wir messen, wir verstecken nicht. | | | |
33
33
 
34
- **NETTO: Mehrkosten** ⚠️ — **+8172 Tokens / Request**, **+22.55 €** auf 1,000 Requests, kumulativ **+102.15%** vs. Baseline.
34
+ **NETTO: Mehrkosten** ⚠️ — **+7799 Tokens / Request**, kumulativ **+97.49%** vs. Baseline.
35
35
 
36
36
  ## Panel B — Verhalten (mit vs. ohne)
37
37
 
@@ -42,7 +42,7 @@ Vier reale Vergleiche aus echten Bench-Runs. Hier liegt der Wert, den Tokens all
42
42
  | Right-skill selection / Richtige Skill-Wahl | Wie oft das passende Skill aktiviert wird (top-K Treffer). | 50.0% | 0.0% | 50.0% | ✅ live |
43
43
  | Destructive-op stops / Stopps bei riskanten Aktionen | Wie oft der Agent vor destructive ops anhält / nachfragt (von 5). | — | — | — | ⚠️ dry-run |
44
44
  | Ask-vs-act ratio / Fragen vs. Handeln | Verhältnis Rückfragen zu Aktionen — niedriger = entschlossener. | 0.000 | 0.000 | 0.000 | ✅ live |
45
- | Task completion rate / Aufgaben fertig | Anteil der Aufgaben, die der Agent vollständig abschließt. | 84.6% | 7.7% | 76.9% | ✅ live |
45
+ | Task completion rate / Aufgaben fertig | Anteil der Aufgaben, die der Agent vollständig abschließt. | 84.6% | 0.0% | 84.6% | ✅ live |
46
46
 
47
47
  ## Glossar
48
48
 
@@ -55,7 +55,7 @@ Plain-language Definitionen für den nicht-Entwickler-Reader.
55
55
  - **rtk** — der *Rust Token Killer*, ein CLI-Wrapper, der verbose Output (`git status`, lint-Output, test-Runner) filtert, bevor das Modell ihn liest. Spart Input-Tokens auf Tool-Calls.
56
56
  - **terse / telegraph** — ein Stil (kurze Phrasen, weggelassene Artikel), den der Agent für knappere Antworten nutzt. Spart Output-Tokens — wenn der Korpus es belohnt.
57
57
  - **Ohne Paket / Mit Paket** — *without the package* / *with the package* — die zwei Arme des A/B-Vergleichs.
58
- - **€-per-1k-requests** — Token-Kosten auf der Referenz-Skala (1.000 Requests durchschnittlicher Größe, gepreist mit den aktuellen Sonnet-Raten aus `internal/bench/pricing.yaml`).
58
+ - **Δ Tokens** — Input-Token-Differenz pro Request gegenüber der Baseline. Bewusst die einzige Kosten-Einheit: ein €-Vergleich würde Per-Request-API-Preise unterstellen, die Abo-Nutzer nicht zahlen.
59
59
 
60
60
  ## Methodik & Quellen
61
61
 
@@ -77,8 +77,8 @@ Diese Seite ist eine **abgeleitete** Sicht — keine eigene Messung. Sie fasst d
77
77
 
78
78
  **Hinweise aus dem Report:**
79
79
 
80
- - Token→€ conversion priced at sonnet rates from internal/bench/pricing.yaml (sourced_on=2026-05-14).
80
+ - Cost is reported in tokens only no € figure. Per-call API pricing misleads subscription users; tokens are the currency-neutral metric.
81
81
  - Pending rungs contribute 0 to the cumulative until measured.
82
82
  - Reference scale: 1000 requests × 8000 input / 600 output tokens per request.
83
83
 
84
- _Last rendered: `2026-05-29T04:36:04+00:00`_
84
+ _Last rendered: `2026-05-31T14:37:17+00:00`_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "5.4.1",
3
+ "version": "5.6.0",
4
4
  "description": "Universal AI Agent OS \u2014 audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -165,6 +165,8 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
165
165
  --payload <path|event-name> [--native-event <native>]
166
166
  [--manifest <path>] [--json] [--dry-run]
167
167
  memory:lookup Retrieve memory entries (text or JSON envelope)
168
+ linked-projects:list List opted-in IDE-attached sibling repos (path · detected_via · large)
169
+ Flags: --all (show undecided too), --format json
168
170
  memory:signal Append a provisional intake signal (memory proposal)
169
171
  memory:hash Hash a memory entry (YAML or JSON stdin)
170
172
  memory:check Validate memory YAML schema + staleness
@@ -419,6 +421,13 @@ cmd_memory_lookup() {
419
421
  exec python3 "$script" "$@"
420
422
  }
421
423
 
424
+ cmd_linked_projects_list() {
425
+ require_python3
426
+ local script
427
+ script="$(resolve_script "scripts/linked_projects_list.py")" || return 1
428
+ exec python3 "$script" "$@"
429
+ }
430
+
422
431
  cmd_memory_signal() {
423
432
  require_python3
424
433
  local script
@@ -928,6 +937,7 @@ main() {
928
937
  implement-ticket) cmd_implement_ticket "$@" ;;
929
938
  work) cmd_work "$@" ;;
930
939
  memory:lookup) cmd_memory_lookup "$@" ;;
940
+ linked-projects:list) cmd_linked_projects_list "$@" ;;
931
941
  memory:signal) cmd_memory_signal "$@" ;;
932
942
  memory:hash) cmd_memory_hash "$@" ;;
933
943
  memory:check) cmd_memory_check "$@" ;;
@@ -52,31 +52,32 @@ def _selection_section(selection: dict[str, Any]) -> str:
52
52
  return "\n".join(lines)
53
53
 
54
54
 
55
- def _cost_section(cost: dict[str, Any]) -> str:
55
+ def _token_usage_section(cost: dict[str, Any]) -> str:
56
+ # Token-only — the monetary (USD) comparison is intentionally omitted:
57
+ # it assumes per-call API pricing, which misleads subscription users.
58
+ # Tokens are the currency-neutral metric that matters. JSON keeps the
59
+ # raw cost field for back-compat; it is simply not rendered.
56
60
  if cost.get("source") == "unavailable":
57
61
  return (
58
- "## Cost capture\n\n"
62
+ "## Token usage\n\n"
59
63
  f"- **source:** `unavailable` ({cost.get('reason', 'unknown')})\n"
60
- f"- **scanned:** `{cost.get('scanned_path', '—')}`\n"
61
- f"- **pricing sourced on:** {cost.get('pricing_sourced_on') or '—'}\n\n"
64
+ f"- **scanned:** `{cost.get('scanned_path', '—')}`\n\n"
62
65
  "_No session jsonl available. Run `node scripts/cost/track.mjs` "
63
66
  "from a real Claude Code session to populate agents/cost-tracking/sessions.jsonl._\n"
64
67
  )
65
68
  totals = cost["totals"]
66
69
  lines = [
67
- "## Cost capture",
70
+ "## Token usage",
68
71
  "",
69
72
  f"- **source:** `{cost['source']}` · sessions scanned: **{cost['sessions_scanned']}**",
70
- f"- **pricing sourced on:** {cost.get('pricing_sourced_on') or '—'}",
71
- f"- **total cost:** **${totals['total_cost_usd']:.6f}**",
72
73
  "",
73
- "| tier | messages | cost (USD) |",
74
- "|---|---:|---:|",
74
+ "| tier | messages |",
75
+ "|---|---:|",
75
76
  ]
76
77
  for tier, slot in cost["per_tier"].items():
77
78
  if slot["messages"] == 0 and slot["cost_usd"] == 0.0:
78
79
  continue
79
- lines.append(f"| {tier} | {slot['messages']} | ${slot['cost_usd']:.6f} |")
80
+ lines.append(f"| {tier} | {slot['messages']} |")
80
81
  lines += [
81
82
  "",
82
83
  "| metric | value |",
@@ -125,21 +126,19 @@ def render_markdown(report: dict[str, Any]) -> str:
125
126
  f"# Benchmark Report — `{corpus['id']}` · {report['generated_at']}\n\n"
126
127
  "## Headline\n\n"
127
128
  f"- **selection** {sel['selection_accuracy']:.2%} (target {sel['target']:.2%}) → **{verdict['selection']}**\n"
128
- f"- **cost** ${cost['totals']['total_cost_usd']:.6f} "
129
- f"({'sessions=' + str(cost['sessions_scanned']) if cost['source'] != 'unavailable' else cost['source']})\n"
129
+ f"- **tokens** {'sessions=' + str(cost['sessions_scanned']) if cost['source'] != 'unavailable' else cost['source']}\n"
130
130
  f"- **quality** {qual['quality_score']:.2%} → **{verdict['quality']}**\n"
131
131
  f"- **overall** → **{verdict['overall']}**\n"
132
132
  )
133
133
  notes = (
134
134
  "## Notes\n\n"
135
135
  f"- corpus path: `{corpus['path']}` · prompts: **{corpus['prompt_count']}**\n"
136
- f"- pricing: `internal/bench/pricing.yaml`\n"
137
136
  f"- baseline collector: `{report['runner']['baseline_collector']}`\n"
138
137
  )
139
138
  return "\n\n".join([
140
139
  headline,
141
140
  _selection_section(sel),
142
- _cost_section(cost),
141
+ _token_usage_section(cost),
143
142
  _quality_section(qual),
144
143
  notes,
145
144
  ]) + "\n"
@@ -104,8 +104,7 @@ def render_telegraph_markdown(report: dict[str, Any]) -> str:
104
104
  f"(p10 {_fmt_pct(agg['savings_vs_terse']['p10'])} · p90 {_fmt_pct(agg['savings_vs_terse']['p90'])})",
105
105
  f"- median realised carve-out share (condensed arm): **{_fmt_pct(agg['realised_carve_out_pct']['median'])}** "
106
106
  f"(expected median {_fmt_pct(agg['expected_carve_out_pct']['median'])})",
107
- f"- total cost: **${cost['totals']['total_cost_usd']:.6f}** "
108
- f"(calls {cost['totals']['calls']} · errors {cost['totals']['errors']})",
107
+ f"- calls: **{cost['totals']['calls']}** · errors: **{cost['totals']['errors']}**",
109
108
  f"- verdict: **{report['verdict']['overall']}**",
110
109
  "",
111
110
  ]
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """Real-tokenizer counting for the budget tooling (roadmap 0B.1).
3
+
4
+ `char != token`. Every budget in this suite is historically in characters;
5
+ the lean-initial-context goal is tokens. This helper adds a token count
6
+ *alongside* chars so chars stay the cheap, stdlib-only proxy and tokens
7
+ become the truth where a real tokenizer is available.
8
+
9
+ Design — no silent installs, no mandatory network (per `missing-tool-handling`):
10
+
11
+ - **GPT** — exact via `tiktoken` (`o200k_base`, the GPT-4o/4.1 encoding) when
12
+ the optional dependency is installed; otherwise a documented `chars / 4`
13
+ proxy flagged `exact=False`. Install `tiktoken` to activate exact counts.
14
+ - **Claude** — no offline tokenizer ships in `anthropic` 0.98 (the SDK exposes
15
+ only the live `messages.count_tokens` endpoint, which needs an API call).
16
+ Offline we use a documented `chars / 3.6` proxy flagged `exact=False`; the
17
+ exact API count is reserved for the live-bench boundaries to avoid spend on
18
+ the cheap path.
19
+
20
+ Both proxies are intentionally conservative ratios drawn from English-prose +
21
+ markdown samples; they are estimates, never gates. The char budgets remain the
22
+ enforced floor (`measure_rule_budget --kernel-budget-check`).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+
29
+ # Proxy ratios (chars per token) for the no-tokenizer fallback. Tuned for
30
+ # English markdown rule/skill prose; deliberately conservative.
31
+ _GPT_CHARS_PER_TOKEN = 4.0
32
+ _CLAUDE_CHARS_PER_TOKEN = 3.6
33
+
34
+ _TIKTOKEN_ENCODING = "o200k_base" # GPT-4o / GPT-4.1 family.
35
+
36
+ # Resolve the optional tiktoken encoder once at import.
37
+ try: # pragma: no cover - exercised by env presence, not unit tests
38
+ import tiktoken # type: ignore
39
+
40
+ _ENC = tiktoken.get_encoding(_TIKTOKEN_ENCODING)
41
+ except Exception: # ImportError, or model-data fetch failure offline
42
+ _ENC = None
43
+
44
+ TIKTOKEN_AVAILABLE = _ENC is not None
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class TokenCount:
49
+ """A single token measurement and whether it is exact or a proxy."""
50
+
51
+ tokens: int
52
+ exact: bool
53
+
54
+
55
+ def gpt_tokens(text: str) -> TokenCount:
56
+ """GPT token count — exact via tiktoken when present, else a char proxy."""
57
+ if _ENC is not None:
58
+ return TokenCount(len(_ENC.encode(text)), True)
59
+ return TokenCount(round(len(text) / _GPT_CHARS_PER_TOKEN), False)
60
+
61
+
62
+ def claude_tokens(text: str) -> TokenCount:
63
+ """Claude token count — documented offline proxy (no local tokenizer)."""
64
+ return TokenCount(round(len(text) / _CLAUDE_CHARS_PER_TOKEN), False)
65
+
66
+
67
+ def measure(text: str) -> dict[str, object]:
68
+ """Return chars + per-model token counts for one text blob.
69
+
70
+ Keys: chars, tokens_gpt, tokens_gpt_exact, tokens_claude,
71
+ tokens_claude_exact. The `*_exact` booleans tell a report consumer
72
+ whether the number is a real tokenizer count or a proxy estimate.
73
+ """
74
+ g = gpt_tokens(text)
75
+ c = claude_tokens(text)
76
+ return {
77
+ "chars": len(text),
78
+ "tokens_gpt": g.tokens,
79
+ "tokens_gpt_exact": g.exact,
80
+ "tokens_claude": c.tokens,
81
+ "tokens_claude_exact": c.exact,
82
+ }
83
+
84
+
85
+ def method_note() -> str:
86
+ """One-line provenance of how token counts were produced (for reports)."""
87
+ if TIKTOKEN_AVAILABLE:
88
+ return (
89
+ f"tokens_gpt: exact (tiktoken {_TIKTOKEN_ENCODING}); "
90
+ f"tokens_claude: proxy (chars/{_CLAUDE_CHARS_PER_TOKEN})"
91
+ )
92
+ return (
93
+ f"tokens_gpt: proxy (chars/{_GPT_CHARS_PER_TOKEN}, tiktoken not installed); "
94
+ f"tokens_claude: proxy (chars/{_CLAUDE_CHARS_PER_TOKEN})"
95
+ )
@@ -333,9 +333,9 @@ def assemble_value_v1(
333
333
  "totals": totals,
334
334
  "notes": [
335
335
  (
336
- "Token→€ conversion priced at "
337
- f"{tier} rates from internal/bench/pricing.yaml "
338
- f"(sourced_on={ref.get('pricing_sourced_on', '—')})."
336
+ "Cost is reported in tokens only — no € figure. Per-call API "
337
+ "pricing misleads subscription users; tokens are the "
338
+ "currency-neutral metric."
339
339
  ),
340
340
  "Pending rungs contribute 0 to the cumulative until measured.",
341
341
  (
@@ -51,7 +51,49 @@ aiv_higgsfield_capability() {
51
51
  "$(higgsfield_audio_for_preset "${preset}")" "${preset}"
52
52
  return 0
53
53
  fi
54
- printf '{"audio":"per-model","presets":["mix","burst","dvd","cinematic","talk"]}\n'
54
+ printf '{"audio":"per-model","presets":["mix","burst","dvd","cinematic","talk"],"speak":true}\n'
55
+ }
56
+
57
+ # --- live helpers -----------------------------------------------------
58
+ # Higgsfield API (authoritative contract — official higgsfield-js SDK):
59
+ # base https://platform.higgsfield.ai
60
+ # auth Authorization: Key <KEY_ID>:<KEY_SECRET> (api-key + api-key-secret)
61
+ # upload POST /api/v1/upload_file (multipart) -> hosted image URL
62
+ # submit POST /v1/image2video/dop -> { request_id, status_url }
63
+ # poll GET /requests/<id>/status -> { status, video:{url} }
64
+ # Fields tagged ASSUMED are documented-best-effort and verified on the
65
+ # first live smoke (this adapter has no captured smoke trace yet).
66
+ HF_BASE_DEFAULT="https://platform.higgsfield.ai"
67
+
68
+ _hf_secret() {
69
+ _aiv_xpath "(/ai-video/provider[@id='${ADAPTER_ID}']|/ai-video/extra/provider[@id='${ADAPTER_ID}'])/api-key-secret"
70
+ }
71
+
72
+ # Documented base; honour AIV_ENDPOINT only when it points at the
73
+ # platform host (the XML default api.higgsfield.ai/v1 is not the SDK base).
74
+ _hf_base() {
75
+ case "${AIV_ENDPOINT:-}" in
76
+ *platform.higgsfield.ai*) printf '%s' "${AIV_ENDPOINT%/}" | sed -E 's#/v1/?$##' ;;
77
+ *) printf '%s' "${HF_BASE_DEFAULT}" ;;
78
+ esac
79
+ }
80
+
81
+ _hf_auth() {
82
+ local secret; secret="$(_hf_secret)"
83
+ [ -n "${secret}" ] || aiv_die 6 "${ADAPTER_ID}: api-key-secret missing in agents/.ai-video.xml"
84
+ command -v aiv_redact_register >/dev/null 2>&1 && aiv_redact_register "${secret}"
85
+ printf 'Authorization: Key %s:%s' "${AIV_KEY}" "${secret}"
86
+ }
87
+
88
+ # image2video needs a DoP video model; the XML default may carry an
89
+ # image model (e.g. higgsfield-soul) — fall back to dop-turbo and warn.
90
+ _hf_model() {
91
+ case "${AIV_MODEL:-}" in
92
+ *dop*|*turbo*) printf '%s' "${AIV_MODEL}" ;;
93
+ *) printf 'dop-turbo'
94
+ printf '%s: XML model "%s" is not a DoP video model; using dop-turbo for image2video\n' \
95
+ "${ADAPTER_ID}" "${AIV_MODEL:-unset}" >&2 ;;
96
+ esac
55
97
  }
56
98
 
57
99
  aiv_cmd_submit() {
@@ -60,29 +102,144 @@ aiv_cmd_submit() {
60
102
  aiv_load_provider "${ADAPTER_ID}"
61
103
  [ "$(aiv_key_status)" = "present" ] \
62
104
  || aiv_die 6 "${ADAPTER_ID}: api key missing in agents/.ai-video.xml"
63
- aiv_die 9 "${ADAPTER_ID}: live submit not yet wired"
105
+
106
+ local stdin_json base auth model ref img_url prompt req resp http body rid
107
+ stdin_json="$(cat)"
108
+ base="$(_hf_base)"; auth="$(_hf_auth)"; model="$(_hf_model)"
109
+
110
+ # image2video must animate a still — require ref_images[0].
111
+ ref="$(printf '%s' "${stdin_json}" | jq -r '.ref_images[0] // empty')"
112
+ [ -n "${ref}" ] || aiv_die 7 "${ADAPTER_ID}: image2video requires ref_images[0] (the still to animate)"
113
+
114
+ case "${ref}" in
115
+ http://*|https://*) img_url="${ref}" ;;
116
+ *)
117
+ case "${ref}" in /*) : ;; *) ref="$(pwd)/${ref}" ;; esac
118
+ [ -f "${ref}" ] || aiv_die 7 "${ADAPTER_ID}: ref image not found: ${ref}"
119
+ # Upload local still -> hosted URL. Multipart field name ASSUMED 'file'.
120
+ local up up_code up_body
121
+ up="$(curl -sS -w '\n%{http_code}' -X POST "${base}/api/v1/upload_file" \
122
+ -H "${auth}" -F "file=@${ref}")" \
123
+ || aiv_die 8 "${ADAPTER_ID}: upload_file curl failed"
124
+ up_code="$(printf '%s' "${up}" | tail -n1)"; up_body="$(printf '%s' "${up}" | sed '$d')"
125
+ case "${up_code}" in 2*) : ;; *) aiv_die 8 "${ADAPTER_ID}: upload HTTP ${up_code}: $(printf '%s' "${up_body}" | head -c 200)" ;; esac
126
+ img_url="$(printf '%s' "${up_body}" | jq -r '.url // .image_url // .file_url // .data.url // empty')"
127
+ [ -n "${img_url}" ] || aiv_die 8 "${ADAPTER_ID}: no URL in upload response (got: $(printf '%s' "${up_body}" | head -c 200))"
128
+ ;;
129
+ esac
130
+
131
+ # DoP wants a camera-movement prompt, not the full scene prose.
132
+ prompt="$(printf '%s' "${stdin_json}" | jq -r '
133
+ [.prompt.camera, .prompt.action, .prompt.mood]
134
+ | map(select(. != null and . != "")) | join(". ")')"
135
+ [ -n "${prompt}" ] || prompt="Cinematic camera movement"
136
+
137
+ # Live API wraps the request in a "params" object (verified: a flat
138
+ # body returns 422 'body.params required'; params requires prompt +
139
+ # input_images). model lives inside params.
140
+ req="$(jq -n --arg m "${model}" --arg p "${prompt}" --arg u "${img_url}" \
141
+ '{params:{model:$m, prompt:$p, input_images:[{type:"image_url", image_url:$u}]}}')"
142
+
143
+ resp="$(curl -sS -w '\n%{http_code}' -X POST "${base}/v1/image2video/dop" \
144
+ -H "${auth}" -H "Content-Type: application/json" --data-binary "${req}")" \
145
+ || aiv_die 8 "${ADAPTER_ID}: image2video curl failed"
146
+ http="$(printf '%s' "${resp}" | tail -n1)"; body="$(printf '%s' "${resp}" | sed '$d')"
147
+ case "${http}" in 2*) : ;; *) aiv_die 8 "${ADAPTER_ID}: submit HTTP ${http}: $(printf '%s' "${body}" | jq -r '.detail // .error // .message // "unknown"' 2>/dev/null | head -c 300)" ;; esac
148
+
149
+ rid="$(printf '%s' "${body}" | jq -r '.request_id // .generation_id // .id // empty')"
150
+ [ -n "${rid}" ] || aiv_die 8 "${ADAPTER_ID}: no request_id in submit response (got: $(printf '%s' "${body}" | head -c 200))"
151
+ jq -n --arg id "${rid}" '{job_id:$id}'
152
+ }
153
+
154
+ # Reconstruct the status URL from the request id (status_url also returned by submit).
155
+ _hf_status_json() {
156
+ local job_id="${1}" base auth resp http body
157
+ base="$(_hf_base)"; auth="$(_hf_auth)"
158
+ resp="$(curl -sS -w '\n%{http_code}' -X GET "${base}/requests/${job_id}/status" -H "${auth}")" \
159
+ || aiv_die 8 "${ADAPTER_ID}: status curl failed"
160
+ http="$(printf '%s' "${resp}" | tail -n1)"; body="$(printf '%s' "${resp}" | sed '$d')"
161
+ case "${http}" in 2*) : ;; *) aiv_die 8 "${ADAPTER_ID}: status HTTP ${http}" ;; esac
162
+ printf '%s' "${body}"
64
163
  }
65
164
 
66
165
  aiv_cmd_poll() {
67
166
  local job_id="${1:-}"
68
167
  [ -n "${job_id}" ] || aiv_die 2 "${ADAPTER_ID}: poll <job_id> required"
69
168
  aiv_assert_dryrun
70
- aiv_die 9 "${ADAPTER_ID}: live poll not yet wired (job=${job_id})"
169
+ aiv_require_cmd curl jq
170
+ aiv_load_provider "${ADAPTER_ID}"
171
+ local st; st="$(_hf_status_json "${job_id}" | jq -r '.status // empty')"
172
+ case "${st}" in
173
+ completed|done|success) printf '{"status":"done"}\n' ;;
174
+ queued) printf '{"status":"queued"}\n' ;;
175
+ in_progress|running|processing) printf '{"status":"running"}\n' ;;
176
+ failed|nsfw|canceled|cancelled) printf '{"status":"failed","reason":"%s"}\n' "${st}" ;;
177
+ *) printf '{"status":"running","raw":"%s"}\n' "${st:-unknown}" ;;
178
+ esac
71
179
  }
72
180
 
73
181
  aiv_cmd_fetch() {
74
182
  local job_id="${1:-}"
75
183
  [ -n "${job_id}" ] || aiv_die 2 "${ADAPTER_ID}: fetch <job_id> required"
76
184
  aiv_assert_dryrun
77
- aiv_die 9 "${ADAPTER_ID}: live fetch not yet wired (job=${job_id})"
185
+ aiv_require_cmd curl jq
186
+ aiv_load_provider "${ADAPTER_ID}"
187
+ local body url out
188
+ body="$(_hf_status_json "${job_id}")"
189
+ url="$(printf '%s' "${body}" | jq -r '.video.url // .results.raw.url // .video_url // (.images[0].url) // empty')"
190
+ [ -n "${url}" ] || aiv_die 8 "${ADAPTER_ID}: no video url in status (status=$(printf '%s' "${body}" | jq -r '.status // "?"'))"
191
+ out="${AIV_OUT:-}"; [ -n "${out}" ] || out="$(mktemp -t aiv-hf-XXXXXX).mp4"
192
+ curl -sS -L -o "${out}" "${url}" || aiv_die 8 "${ADAPTER_ID}: download failed: ${url}"
193
+ case "${out}" in /*) : ;; *) out="$(pwd)/${out}" ;; esac
194
+ jq -n --arg p "${out}" '{video_path:$p, audio_embedded:false}'
195
+ }
196
+
197
+ # aiv_cmd_speak — audio-driven lip-sync. Stdin JSON:
198
+ # {input_image: <url>, input_audio: <wav url>, prompt: <string>}
199
+ # Animates the portrait's mouth to the supplied vocal WAV via
200
+ # POST /v1/speak/higgsfield. Returns {job_id}; poll/fetch are shared.
201
+ # Image + audio MUST be public URLs (the platform upload endpoint is
202
+ # WAF-gated for non-browser clients). Audio must be WAV.
203
+ aiv_cmd_speak() {
204
+ aiv_assert_dryrun
205
+ aiv_require_cmd curl jq
206
+ aiv_load_provider "${ADAPTER_ID}"
207
+ [ "$(aiv_key_status)" = "present" ] \
208
+ || aiv_die 6 "${ADAPTER_ID}: api key missing in agents/.ai-video.xml"
209
+ local stdin_json base auth img aud prompt req resp http body rid
210
+ stdin_json="$(cat)"
211
+ base="$(_hf_base)"; auth="$(_hf_auth)"
212
+ img="$(printf '%s' "${stdin_json}" | jq -r '.input_image // .image_url // (.ref_images[0]?) // empty')"
213
+ aud="$(printf '%s' "${stdin_json}" | jq -r '.input_audio // .audio_url // empty')"
214
+ prompt="$(printf '%s' "${stdin_json}" | jq -r 'if (.prompt|type)=="string" then .prompt else empty end')"
215
+ [ -n "${prompt}" ] || prompt="sing the line with force, mouth moving precisely to the words"
216
+ [ -n "${img}" ] || aiv_die 7 "${ADAPTER_ID}: speak requires input_image (public URL)"
217
+ [ -n "${aud}" ] || aiv_die 7 "${ADAPTER_ID}: speak requires input_audio (public WAV URL)"
218
+ case "${img}" in http://*|https://*) : ;; *) aiv_die 7 "${ADAPTER_ID}: speak input_image must be a public URL (local upload is WAF-gated)" ;; esac
219
+ case "${aud}" in http://*|https://*) : ;; *) aiv_die 7 "${ADAPTER_ID}: speak input_audio must be a public WAV URL" ;; esac
220
+ req="$(jq -n --arg i "${img}" --arg a "${aud}" --arg p "${prompt}" \
221
+ '{params:{input_image:{type:"image_url",image_url:$i},input_audio:{type:"audio_url",audio_url:$a},prompt:$p}}')"
222
+ resp="$(curl -sS -w '\n%{http_code}' -X POST "${base}/v1/speak/higgsfield" \
223
+ -H "${auth}" -H "Content-Type: application/json" --data-binary "${req}")" \
224
+ || aiv_die 8 "${ADAPTER_ID}: speak curl failed"
225
+ http="$(printf '%s' "${resp}" | tail -n1)"; body="$(printf '%s' "${resp}" | sed '$d')"
226
+ case "${http}" in 2*) : ;; *) aiv_die 8 "${ADAPTER_ID}: speak HTTP ${http}: $(printf '%s' "${body}" | jq -r '.detail // .error // .message // "unknown"' 2>/dev/null | head -c 300)" ;; esac
227
+ rid="$(printf '%s' "${body}" | jq -r '.id // .request_id // empty')"
228
+ [ -n "${rid}" ] || aiv_die 8 "${ADAPTER_ID}: speak: no request_id (got: $(printf '%s' "${body}" | head -c 200))"
229
+ jq -n --arg id "${rid}" '{job_id:$id}'
78
230
  }
79
231
 
80
- # Custom dispatch with capability override; falls back to the common
81
- # router for every other subcommand.
232
+ # Custom dispatch: capability + speak handled here; submit/poll/fetch/
233
+ # run/dry-run fall through to the common router.
82
234
  sub="${1:-}"
83
235
  if [ "${sub}" = "capability" ]; then
84
236
  shift
85
237
  aiv_higgsfield_capability "$@"
86
238
  exit 0
87
239
  fi
240
+ if [ "${sub}" = "speak" ]; then
241
+ shift
242
+ aiv_cmd_speak "$@"
243
+ exit 0
244
+ fi
88
245
  aiv_dispatch "${ADAPTER_ID}" "per-model" "$@"
@@ -42,12 +42,98 @@ aiv_cmd_run() {
42
42
  seed="$(printf '%s' "${stdin_json}" | jq -r '.seed // empty')"
43
43
  ref_first="$(printf '%s' "${stdin_json}" | jq -r '.ref_images[0] // empty')"
44
44
 
45
- # Live mode is implementation-scaffolded: we do NOT yet POST to the
46
- # OpenAI Images API from inside this adapter. The cost-floor gate
47
- # (Phase 5 Step 6) refuses live mode unless the operator explicitly
48
- # confirms in-turn; this branch surfaces a clear stub error so the
49
- # contract surface stays stable while the live path is wired later.
50
- aiv_die 9 "${ADAPTER_ID}: live mode not yet wired (prompt=${#prompt} chars, seed=${seed:-unset}, ref=${ref_first:-none})"
45
+ [ -n "${prompt}" ] || aiv_die 7 "${ADAPTER_ID}: empty prompt (prompt.* blocks required)"
46
+
47
+ # Images-generations API has no negative-prompt field fold the
48
+ # negative list into the prompt as an explicit "Avoid:" clause.
49
+ local negative
50
+ negative="$(printf '%s' "${stdin_json}" | jq -r '(.negative // []) | join(", ")')"
51
+ [ -n "${negative}" ] && prompt="${prompt} Avoid: ${negative}."
52
+
53
+ # gpt-image-1 has no seed param. Log if present.
54
+ [ -n "${seed}" ] && printf '%s: seed=%s ignored (gpt-image-1 has no seed)\n' "${ADAPTER_ID}" "${seed}" >&2
55
+ : "${ref_first:=}"
56
+
57
+ # Resolve size from requested aspect (stdin .aspect overrides XML tuning).
58
+ local aspect quality size out
59
+ aspect="$(printf '%s' "${stdin_json}" | jq -r --arg a "${AIV_TUNING_ASPECT:-16:9}" '.aspect // $a')"
60
+ quality="${AIV_TUNING_QUALITY:-high}"
61
+ case "${aspect}" in
62
+ 16:9|3:2|landscape) size="1536x1024" ;;
63
+ 9:16|2:3|portrait) size="1024x1536" ;;
64
+ 1:1|square) size="1024x1024" ;;
65
+ *) size="1536x1024" ;;
66
+ esac
67
+
68
+ # Output path: caller-set AIV_OUT wins; else a temp PNG.
69
+ out="${AIV_OUT:-}"
70
+ [ -n "${out}" ] || out="$(mktemp -t aiv-openai-XXXXXX).png"
71
+
72
+ # Collect reference image files. When present → /v1/images/edits
73
+ # (reference-conditioned, so the model adheres to the supplied
74
+ # character); otherwise plain text-to-image /v1/images/generations.
75
+ local -a ref_files=() tmp_files=()
76
+ local r tmp
77
+ while IFS= read -r r; do
78
+ [ -n "${r}" ] || continue
79
+ case "${r}" in
80
+ http://*|https://*)
81
+ tmp="$(mktemp -t aiv-ref-XXXXXX).png"
82
+ curl -sS -L -o "${tmp}" "${r}" || aiv_die 8 "${ADAPTER_ID}: failed to download ref image: ${r}"
83
+ ref_files+=("${tmp}"); tmp_files+=("${tmp}") ;;
84
+ *)
85
+ case "${r}" in /*) : ;; *) r="$(pwd)/${r}" ;; esac
86
+ [ -f "${r}" ] || aiv_die 7 "${ADAPTER_ID}: ref image not found: ${r}"
87
+ ref_files+=("${r}") ;;
88
+ esac
89
+ done < <(printf '%s' "${stdin_json}" | jq -r '.ref_images[]? // empty')
90
+
91
+ local req resp http_code body b64
92
+ if [ "${#ref_files[@]}" -gt 0 ]; then
93
+ # Reference-conditioned edit. gpt-image-1 accepts multiple image[] refs.
94
+ local -a fargs=(-F "model=${AIV_MODEL:-gpt-image-1}" -F "prompt=${prompt}" \
95
+ -F "size=${size}" -F "quality=${quality}" -F "n=1")
96
+ for r in "${ref_files[@]}"; do fargs+=(-F "image[]=@${r};type=image/png"); done
97
+ printf '%s: edits endpoint with %d reference image(s)\n' "${ADAPTER_ID}" "${#ref_files[@]}" >&2
98
+ resp="$(curl -sS -w '\n%{http_code}' \
99
+ -X POST "${AIV_ENDPOINT%/}/images/edits" \
100
+ -H "Authorization: Bearer ${AIV_KEY}" \
101
+ "${fargs[@]}")" \
102
+ || aiv_die 8 "${ADAPTER_ID}: curl to ${AIV_ENDPOINT%/}/images/edits failed"
103
+ else
104
+ req="$(jq -n \
105
+ --arg m "${AIV_MODEL:-gpt-image-1}" --arg p "${prompt}" \
106
+ --arg s "${size}" --arg q "${quality}" \
107
+ '{model: $m, prompt: $p, size: $s, quality: $q, n: 1}')"
108
+ resp="$(curl -sS -w '\n%{http_code}' \
109
+ -X POST "${AIV_ENDPOINT%/}/images/generations" \
110
+ -H "Authorization: Bearer ${AIV_KEY}" \
111
+ -H "Content-Type: application/json" \
112
+ --data-binary "${req}")" \
113
+ || aiv_die 8 "${ADAPTER_ID}: curl to ${AIV_ENDPOINT%/}/images/generations failed"
114
+ fi
115
+ # Clean up any downloaded temp refs (set -u safe on empty arrays).
116
+ for tmp in ${tmp_files[@]+"${tmp_files[@]}"}; do rm -f "${tmp}"; done
117
+
118
+ http_code="$(printf '%s' "${resp}" | tail -n1)"
119
+ body="$(printf '%s' "${resp}" | sed '$d')"
120
+ case "${http_code}" in
121
+ 2*) : ;;
122
+ *) aiv_die 8 "${ADAPTER_ID}: HTTP ${http_code}: $(printf '%s' "${body}" | jq -r '.error.message // .error // "unknown error"' 2>/dev/null | head -c 300)" ;;
123
+ esac
124
+
125
+ # gpt-image-1 always returns base64 (no url).
126
+ b64="$(printf '%s' "${body}" | jq -r '.data[0].b64_json // empty')"
127
+ [ -n "${b64}" ] || aiv_die 8 "${ADAPTER_ID}: no image data in response (got: $(printf '%s' "${body}" | head -c 200))"
128
+
129
+ # Portable base64 decode (GNU -d / BSD -D).
130
+ local b64dec
131
+ if printf '' | base64 -d >/dev/null 2>&1; then b64dec='base64 -d'; else b64dec='base64 -D'; fi
132
+ printf '%s' "${b64}" | ${b64dec} > "${out}" \
133
+ || aiv_die 8 "${ADAPTER_ID}: base64 decode to ${out} failed"
134
+
135
+ case "${out}" in /*) : ;; *) out="$(pwd)/${out}" ;; esac
136
+ jq -n --arg p "${out}" '{video_path: $p, audio_embedded: false}'
51
137
  }
52
138
 
53
139
  aiv_cmd_submit() { aiv_cmd_run "$@"; }