diff --git a/.docs/implementation-plan-pr-backlog-2026-04-09.md b/.docs/implementation-plan-pr-backlog-2026-04-09.md new file mode 100644 index 000000000..0da932b8b --- /dev/null +++ b/.docs/implementation-plan-pr-backlog-2026-04-09.md @@ -0,0 +1,316 @@ +# Implementation Plan: PR Backlog Prioritization & Merge Strategy + +**Status**: Draft +**Research Doc**: `.docs/research-pr-backlog-2026-04-09.md` +**Author**: AI Analysis +**Date**: 2026-04-09 +**Estimated Effort**: 3-4 hours (execution time, not including verification) + +## Overview + +### Summary +Execute a 5-phase strategy to clear the 17-open-PR backlog by consolidating duplicates, merging security/compliance fixes first, then proceeding to infrastructure (without Tauri) and feature work in dependency order. **Tauri is being moved to terraphim-ai-desktop repository.** + +### Approach +1. Reconcile Gitea PR state with actual branch state +2. Close duplicate PRs to prevent merge conflicts +3. Execute merge in priority order (security → compliance → bugfixes → features) +4. Tauri-related PRs (#491) to be closed - Tauri moves to terraphim-ai-desktop + +### Scope + +**In Scope:** +- 17 open PRs across GitHub and Gitea +- Duplicate identification and closure +- Merge sequence execution (5 phases, Tauri removed) +- CI verification + +**Out of Scope:** +- Tauri desktop application (moving to terraphim-ai-desktop) +- Investigating why duplicates were created +- Refactoring ADF swarm architecture +- Adding new features not in backlog + +**Avoid At All Cost:** +- Merging duplicates before closing originals (would cause conflicts) +- Skipping security PRs for "faster" feature work +- Ignoring CI failures to "get things done" +- Merging Tauri-related PRs here (belongs in terraphim-ai-desktop) + +## Architecture + +### Merge Flow Diagram +``` + ┌─────────────────────────────────────────┐ + │ PHASE 1: CLEANUP │ + │ Close duplicates (#496, #503, #776) │ + │ Close #491 (Tauri → desktop repo) │ + └─────────────────┬───────────────────────┘ + ▼ + ┌─────────────────────────────────────────┐ + │ PHASE 2: SECURITY (Priority) │ + │ #486 RUSTSEC + Ollama binding │ + │ #412 Compliance verification │ + └─────────────────┬───────────────────────┘ + ▼ + ┌─────────────────────────────────────────┐ + │ PHASE 3: COMPLIANCE │ + │ #493 Consolidate license fixes │ + └─────────────────┬───────────────────────┘ + ▼ + ┌─────────────────────────────────────────┐ + │ PHASE 4: BUGFIXES │ + │ #475, #477, #508, #512 │ + └─────────────────┬───────────────────────┘ + ▼ + ┌─────────────────────────────────────────┐ + │ PHASE 5: FEATURES │ + │ #520, #405, #519 │ + └─────────────────────────────────────────┘ +``` + +### Key Design Decisions + +| Decision | Rationale | Alternatives Rejected | +|----------|-----------|----------------------| +| Security first | RUSTSEC is active CVE | Features first (rejected - security debt) | +| Close duplicates before merge | Prevent merge conflicts | Merge all, resolve conflicts (rejected - wasteful) | +| Tauri removed from repo | Moving to terraphim-ai-desktop | Keep here (rejected - bloat, separate concerns) | +| Consolidate license PRs | 3 PRs same fix | Merge all 3 (rejected - conflicts) | + +### Eliminated Options + +| Option Rejected | Why Rejected | Risk of Including | +|-----------------|--------------|-------------------| +| Merge all PRs simultaneously | Would cause massive conflicts | Unreleasable state | +| Ignore duplicate PRs | Merge conflicts inevitable | Wasted CI cycles | +| Skip CI verification | Security regressions possible | Production vulnerabilities | +| Merge GitHub #776 separately | Duplicate of Gitea #520 | Confusing state | + +### Simplicity Check + +**What if this could be easy?** +- Just close obvious duplicates first +- Run `cargo audit` and `cargo deny` to verify security +- Use tea CLI one-liners for merges +- If CI passes, merge; if not, debug + +**Answer**: The plan IS simple. The execution is just methodical. + +## File Changes + +This plan does not modify source code. It coordinates GitHub/Gitea PR state. + +### Git Operations + +| Action | Branch | Purpose | +|--------|--------|---------| +| Reconcile state | main | Verify PR vs branch state | +| Push cleanup | main | Sync reconciled state | + +### External State Changes (GitHub/Gitea) + +| PR | Action | Command | +|----|--------|---------| +| GitHub #776 | Close (duplicate) | `gh pr close 776` | +| GitHub #775 | Close (already merged) | `gh pr close 775` | +| Gitea #491 | Close (Tauri moved) | `tea pulls close 491` | +| Gitea #496 | Close (duplicate) | `tea pulls close 496` | +| Gitea #503 | Close (duplicate) | `tea pulls close 503` | + +## API Design + +### tea/Gitea CLI Commands + +```bash +# Close duplicate PRs +tea pulls close {496,503} --repo terraphim/terraphim-ai + +# Merge PRs +tea pulls merge {ID} --repo terraphim/terraphim-ai + +# Check PR status +tea pulls list --repo terraphim/terraphim-ai --state open +``` + +### gh/GitHub CLI Commands + +```bash +# Close duplicate PRs +gh pr close 776 --repo terraphim/terraphim-ai +gh pr close 775 --repo terraphim/terraphim-ai + +# Check PR status +gh pr list --repo terraphim/terraphim-ai --state open +``` + +## Test Strategy + +### Pre-Merge Verification + +| Test | Command | Pass Criteria | +|------|---------|---------------| +| Cargo audit | `cargo audit` | 0 vulnerabilities | +| Cargo deny | `cargo deny check licenses` | 0 failures | +| Clippy | `cargo clippy --workspace` | 0 warnings | +| Format | `cargo fmt -- --check` | 0 failures | +| Build | `cargo build --workspace` | Compiles | + +### Post-Merge Verification + +| Test | Purpose | +|------|---------| +| CI pipeline | All GitHub Actions pass | +| gitea-robot ready | Next PR becomes "ready" | + +## Implementation Steps + +### Step 1: Reconcile PR State +**Purpose**: Verify which PRs are actually open vs already merged +**Command**: `git log --oneline --all | head -50` +**Estimated**: 15 minutes + +### Step 2: Close GitHub Duplicates +**Files**: GitHub remote +**Description**: Close GitHub PRs that duplicate Gitea work +**Commands**: +```bash +gh pr close 776 --repo terraphim/terraphim-ai --comment "Duplicate of Gitea #520" +gh pr close 775 --repo terraphim/terraphim-ai --comment "Already merged in main" +``` +**Estimated**: 5 minutes + +### Step 3: Close Gitea Duplicates +**Files**: Gitea remote +**Description**: Close Gitea PRs that are duplicates +**Commands**: +```bash +tea pulls close 496 --repo terraphim/terraphim-ai --comment "Duplicate of #493" +tea pulls close 503 --repo terraphim/terraphim-ai --comment "Duplicate of #493" +``` +**Estimated**: 5 minutes + +### Step 4: Merge Security PRs +**Files**: Gitea remote +**Description**: Merge RUSTSEC and compliance PRs +**Commands**: +```bash +# Verify first +cargo audit +cargo deny check licenses + +# Then merge +tea pulls merge 486 --repo terraphim/terraphim-ai +tea pulls merge 412 --repo terraphim/terraphim-ai +``` +**Estimated**: 30 minutes (including verification) + +### Step 5: Merge Compliance PR +**Files**: Gitea remote +**Description**: Merge consolidated license fix +**Commands**: +```bash +tea pulls merge 493 --repo terraphim/terraphim-ai +``` +**Estimated**: 10 minutes + +### Step 6: Merge Bugfix PRs +**Files**: Gitea remote +**Description**: Merge low-risk bugfixes +**Commands**: +```bash +tea pulls merge 475 --repo terraphim/terraphim-ai +tea pulls merge 477 --repo terraphim/terraphim-ai +tea pulls merge 508 --repo terraphim/terraphim-ai # if still open +tea pulls merge 512 --repo terraphim/terraphim-ai # if still open +``` +**Estimated**: 20 minutes + +### Step 7: Close Tauri PR +**Files**: Gitea remote +**Description**: Tauri being moved to terraphim-ai-desktop +**Commands**: +```bash +tea pulls close 491 --repo terraphim/terraphim-ai --comment "Tauri moving to terraphim-ai-desktop repository" +``` +**Estimated**: 5 minutes + +### Step 8: Merge Feature PRs +**Files**: Gitea remote +**Description**: Merge final wave of features +**Commands**: +```bash +tea pulls merge 520 --repo terraphim/terraphim-ai # ValidationService +tea pulls merge 405 --repo terraphim/terraphim-ai # Phase 7 (verify phase completion first) +tea pulls merge 519 --repo terraphim/terraphim-ai # Token tracking +``` +**Estimated**: 45 minutes + +### Step 9: Final Verification +**Purpose**: Ensure main is clean and pushable +**Commands**: +```bash +git pull --rebase gitea main +cargo build --workspace +cargo test --workspace +git push gitea main +``` +**Estimated**: 30 minutes + +## Rollback Plan + +If issues discovered during merge: +1. Stop immediately - do not continue to next PR +2. Revert last merge: `git revert HEAD && git push gitea` +3. Debug issue before resuming +4. Use `git stash` if needed to isolate work + +Feature flag: N/A (PR-level, not code-level) + +## Migration + +N/A - this is a PR coordination plan, not a code migration + +## Dependencies + +### New Dependencies +None - uses existing tea/gh CLI tools + +### Dependency Updates +None + +## Performance Considerations + +### Expected Performance +| Metric | Target | Measurement | +|--------|--------|-------------| +| Time to clear backlog | < 1 week | From now to all PRs closed/merged | +| CI pass rate | 100% | All merged PRs pass CI | +| Merge conflicts | 0 | Confirmed by closing duplicates first | + +## Open Items + +| Item | Status | Owner | +|------|--------|-------| +| Verify #508, #512 actual state | Pending | Git history check | +| Confirm #516 blocks #520 | Pending | Issue investigation | +| Verify #405 phase completion | Pending | Review phase criteria | +| Tauri moved to terraphim-ai-desktop | Confirmed | terraphim-ai-desktop repo | + +## Approval + +- [ ] Research document reviewed and approved +- [ ] Implementation plan reviewed and approved +- [ ] Human approval received +- [ ] Execution can begin + +## Summary + +| Phase | PRs | Action | Est. Time | +|-------|-----|--------|-----------| +| 1 | Duplicates + Tauri | Close 5 PRs | 20 min | +| 2 | Security (#486, #412) | Merge | 30 min | +| 3 | Compliance (#493) | Merge | 10 min | +| 4 | Bugfixes (#475, #477, #508, #512) | Merge | 20 min | +| 5 | Features (#520, #405, #519) | Merge | 45 min | +| **Total** | | | **~2.5 hours** | diff --git a/.docs/research-pr-backlog-2026-04-09.md b/.docs/research-pr-backlog-2026-04-09.md new file mode 100644 index 000000000..bbd396131 --- /dev/null +++ b/.docs/research-pr-backlog-2026-04-09.md @@ -0,0 +1,211 @@ +# Research Document: PR Backlog Prioritization & Merge Strategy + +**Status**: Draft +**Author**: AI Analysis +**Date**: 2026-04-09 +**Reviewers**: [Human Approval Required] + +## Executive Summary + +Analysis of 17 open PRs (2 GitHub, 15 Gitea) reveals a fragmented backlog with duplicate fixes, security remediations at varying stages, and feature work with unclear dependencies. The optimal approach is a 6-phase merge strategy prioritizing security/compliance first, consolidating duplicates, then proceeding to infrastructure and features. + +## Essential Questions Check + +| Question | Answer | Evidence | +|----------|--------|----------| +| Does this energize us? | YES | Critical security and compliance items blocking progress | +| Does it leverage strengths? | YES | Gitea PageRank workflow for prioritization | +| Does it meet a real need? | YES | 17 PRs backlogged, some contain security fixes | + +**Proceed**: YES (3/3 YES) + +## Problem Statement + +### Description +17 open PRs across two remotes (GitHub and Gitea) with: +- Duplicate/overlapping fixes for the same issues +- Security remediations at varying stages of completion +- Feature PRs with unclear dependency chains +- Unclear which PRs are actually still open vs already merged + +### Impact +- Development bottleneck due to PR backlog +- Security vulnerabilities may remain unfixed if PRs are not merged in correct order +- Wasted CI/resources on duplicate PRs +- Risk of merge conflicts if not sequenced properly + +### Success Criteria +- All security/compliance PRs merged first +- Duplicate PRs consolidated or closed +- Clear dependency graph established +- All remaining PRs merged in priority order + +## Current State Analysis + +### Existing Implementation +Git repository with dual-remote setup: +- `gitea` remote: interim work-in-progress +- `origin` remote: release-quality only + +### Remote Configuration +``` +gitea https://git.terraphim.cloud/terraphim/terraphim-ai.git (fetch/push) +origin https://github.com/terraphim/terraphim-ai.git (fetch only shown) +``` + +### Code Locations +| Component | Location | Purpose | +|-----------|----------|---------| +| Security PRs | `task/486-*`, `task/440-*` branches | RUSTSEC-2026-0049 remediation | +| License fixes | `task/493-*`, `task/496-*`, `task/503-*` branches | License field additions | +| ValidationService | `feat/kg-command-validation` branch | Command validation infrastructure | + +### Data Flow +PR lifecycle: +``` +Branch Creation -> Commit -> Push to gitea -> PR Created -> Code Review -> Merge (gitea) -> Sync to origin (release) +``` + +## Constraints + +### Technical Constraints +- Dual-remote architecture requires PRs to be release-quality before GitHub merge +- Security PRs require `security-sentinel` verification pass +- Tauri being removed - moved to terraphim-ai-desktop repository + +### Business Constraints +- RUSTSEC-2026-0049 is a known vulnerability that should be remediated +- License compliance required for CI gates (cargo deny) +- Some PRs duplicate work already done in other branches + +### Non-Functional Requirements +| Requirement | Target | Current | +|-------------|--------|---------| +| Security fixes | 100% merged before feature work | Partial | +| Duplicate resolution | < 3 duplicate PRs remaining | 6+ duplicates | +| Merge queue | < 10 open PRs | 17 open | +| Tauri removal | Complete | Pending | + +## Vital Few (Essentialism) + +### Essential Constraints (Max 3) +1. **Security first**: RUSTSEC-2026-0049 must be remediated before release +2. **No duplicates**: Consolidate license field fixes to single PR +3. **Dependency order**: Infrastructure before features + +### Eliminated from Scope +- Investigating why duplicate PRs were created (post-mortem for later) +- Refactoring existing ADF swarm architecture +- Adding new features not already in PR backlog + +## Dependencies + +### Internal Dependencies +| Dependency | Impact | Risk | +|------------|--------|------| +| ValidationService (#520) | Blocks #516 extension work | Medium | +| Tauri v2 migration (#491) | Required for desktop builds | High | +| Token tracking (#519) | Building block for cost controls | Medium | + +### External Dependencies +| Dependency | Version | Risk | Alternative | +|------------|---------|------|-------------| +| RUSTSEC-2026-0049 | N/A | High - active CVE | Upgrade rustls-webpki | +| Tauri API v2 | Breaking | High | Stay on v1 (blocks desktop) | + +## Risks and Unknowns + +### Known Risks +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Duplicate PR merge conflicts | High | Medium | Close duplicates first | +| RUSTSEC fix incomplete | Medium | Critical | Require security-sentinel pass | +| Tauri v2 breaks desktop | Medium | High | Require platform testing | + +### Open Questions +1. Are PRs #508, #512 actually merged? (commit history suggests yes) +2. Does #516 unblock #520 deployment readiness? +3. What are phase completion criteria for #405 (Phase 7)? +4. Has desktop platform testing for #491 been performed? + +### Assumptions Explicitly Stated +| Assumption | Basis | Risk if Wrong | Verified? | +|------------|-------|---------------|-----------| +| RUSTSEC fix in #486 is complete | Multiple branches reference same CVE | Incomplete fix remains | No | +| License PRs are duplicates | Similar titles, same manifests | Merge conflicts | Partial | +| GitHub #776 is duplicate of Gitea #520 | Both ValidationService | Wasted review effort | Yes | + +## Research Findings + +### Key Insights +1. **Duplicate泛滥**: 6+ PRs address overlapping concerns (license fields, RUSTSEC remediation) +2. **Security debt**: Multiple RUSTSEC-related branches suggest incomplete or contested fixes +3. **Phase confusion**: #405 labeled "Phase 7" but unclear what phases 1-6 are +4. **State inconsistency**: Gitea UI shows PRs as "open" but commit history suggests some are merged + +### Relevant Prior Art +- Gitea PageRank workflow for issue prioritization +- ZDP phased development (Discovery, Define, Design, Develop, Deploy, Drive) +- Dual-remote git workflow (interim vs release) + +### Technical Spikes Needed +| Spike | Purpose | Estimated Effort | +|-------|---------|------------------| +| Verify #486 RUSTSEC fix completeness | Confirm no remaining vulnerabilities | 2 hours | +| Tauri v2 API migration assessment | Confirm desktop compatibility | 4 hours | +| PR state reconciliation | Sync Gitea UI with actual branch state | 1 hour | + +## Recommendations + +### Proceed/No-Proceed +**PROCEED** with 6-phase merge strategy, cleaning duplicates first. + +### Scope Recommendations +1. Close duplicate PRs before attempting any merges +2. Merge security/compliance PRs in dependency order +3. Defer Tauri v2 migration until platform testing confirmed +4. Close GitHub PRs that duplicate Gitea work + +### Risk Mitigation Recommendations +1. Require `cargo audit` and `cargo deny` passes before compliance PR merge +2. Require security-sentinel verification for RUSTSEC fix +3. Perform Tauri v2 migration in staging environment first + +## Next Steps (Upon Approval) + +1. Reconcile Gitea PR state with actual branch state +2. Close duplicate PRs (#496, #503, GitHub #776) +3. Execute 6-phase merge strategy in order +4. Monitor CI/CD for failures + +## Appendix + +### PR Categorization Matrix + +| Type | PRs | Count | Effort | Impact | +|------|-----|-------|--------|--------| +| **Security** | #486, #412 | 2 | Low | Critical | +| **Compliance** | #493, #496, #503 | 3 | Low | High | +| **Bugfix** | #475, #477, #508, #512 | 4 | Low | Medium | +| **Feature** | #405, #519, #520 | 3 | Medium | High | +| **Infrastructure** | #491 | 1 | Medium | High | + +### Recommended Merge Sequence + +| Phase | PRs | Action | +|-------|-----|--------| +| 1 - Cleanup | Duplicates | Close #496, #503, GitHub #776 | +| 2 - Security | #486, #412 | Merge first (critical) | +| 3 - Compliance | #493 | Consolidate license fixes | +| 4 - Bugfixes | #475, #477, #508, #512 | Merge low-risk | +| 5 - Infrastructure | #491 | Tauri v2 with testing | +| 6 - Features | #520, #405, #519 | Final merge wave | + +### PRs to Close Immediately + +| PR | Reason | +|----|--------| +| GitHub #776 | Duplicate of Gitea #520 | +| GitHub #775 | Bench fix appears already merged at main | +| #496 | Duplicate of #493 | +| #503 | Duplicate of #493 | diff --git a/.docs/security-audit-20260421.md b/.docs/security-audit-20260421.md new file mode 100644 index 000000000..2c4417262 --- /dev/null +++ b/.docs/security-audit-20260421.md @@ -0,0 +1,73 @@ +# Security Audit Session 2026-04-21 + +**Agent:** Vigil (security-sentinel) +**Date:** 2026-04-21 +**Branch:** task/696-repl-format-robot-dispatch +**Verdict:** **FAIL** - 4 critical/unsound vulnerabilities persistent + +## CVE Findings + +### Critical (TLS Bypass) +- **RUSTSEC-2026-0098**: rustls-webpki 0.101.7 + 0.102.8 - Name constraints for URI names incorrectly accepted + - Path: serenity 0.12.5 → rustls 0.22.4 → rustls-webpki 0.102.8 + - Affects: TLS validation for domain names in certificates + +- **RUSTSEC-2026-0099**: rustls-webpki 0.101.7 + 0.102.8 - Wildcard name constraints accepted + - Same dependency path as RUSTSEC-2026-0098 + - Blocks: All HTTPS connections until remediated + +### Critical (Cryptographic Unsoundness) +- **RUSTSEC-2026-0097**: rand 0.8.5, 0.9.2, 0.10.0 - Unsound with custom loggers + - Affects: cryptographic randomness in tungstenite, sqlx-postgres, slack-morphism + - Impact: Weakened RNG in Discord bot (serenity), database connections + +### High (Unmaintained) +- **RUSTSEC-2025-0141**: bincode 1.3.3 unmaintained serialisation library +- **RUSTSEC-2024-0384**: instant 0.1.13 unmaintained timing library +- **RUSTSEC-2025-0119**: number_prefix 0.4.0 unmaintained progress bar +- **RUSTSEC-2024-0436**: paste 1.0.15 unmaintained proc-macro helper +- **RUSTSEC-2025-0134**: rustls-pemfile 1.0.4 unmaintained certificate parser +- **RUSTSEC-2020-0163**: term_size 0.3.2 unmaintained terminal size detection + +## Infrastructure Finding + +- **Port 11434 (IPv6 Wildcard)**: `:::11434 LISTEN` + - Ollama exposed on IPv6 0.0.0.0::/0 + - Allows unauthenticated LLM access from network boundary + - Remediation: Bind to 127.0.0.1:11434 only + +## Code Security Assessment + +- **Hardcoded Secrets**: 0 found (safe) +- **Unsafe Blocks**: 0 found (safe-first architecture) +- **Recent Commits**: No security-relevant changes in past 24h + +## Root Cause Analysis + +**Architectural Blocker:** serenity 0.12.5 Discord bot library pins rustls 0.22.4 (December 2023, unmaintained). Upstream fix exists (commit 62b504fc removes serenity) but not merged to this branch. + +**Decision Status:** Awaiting product owner decision on remediation: +- **Option A (Preferred)**: Remove serenity dependency - unblocks all TLS CVEs +- **Option B**: Fork serenity and update rustls dependency +- **Option C**: Accept CVE risk until upstream fix merged + +## Audit History + +This is the **43rd consecutive audit** with identical TLS findings: +- Sessions: 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43+ +- Duration: 40+ cycles (24+ days) +- Remediation Attempts: 0 code changes +- Status: **Persistent, unresolved, awaiting decision** + +## Dispatch Context + +- **Mention Issue**: None (no explicit dispatch context provided) +- **Branch Task**: task/696-repl-format-robot-dispatch (feature task, unrelated) +- **Posting**: This audit documented as standing security mandate + +--- + +**Next Actions:** +1. Escalate serenity decision to product owner +2. If serenity removal approved: remove dependency, re-audit (expect PASS) +3. If deferred: update threshold CVEs for next audit cycle diff --git a/Cargo.lock b/Cargo.lock index e84f0e4d6..a370ad23e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8925,6 +8925,7 @@ dependencies = [ "env_logger", "futures", "grepapp_haystack", + "haystack_jmap", "home", "html2md", "jiff", @@ -9014,6 +9015,7 @@ dependencies = [ "axum 0.8.8", "chrono", "cron", + "glob", "handlebars", "hex", "hmac", diff --git a/crates/terraphim_agent/Cargo.toml b/crates/terraphim_agent/Cargo.toml index 741eadf64..4a742884d 100644 --- a/crates/terraphim_agent/Cargo.toml +++ b/crates/terraphim_agent/Cargo.toml @@ -28,6 +28,7 @@ firecracker = ["repl"] update-tests = [] repl-sessions = ["repl", "dep:terraphim_sessions"] shared-learning = [] +jmap = ["terraphim_middleware/jmap"] [dependencies] anyhow = { workspace = true } diff --git a/crates/terraphim_agent/src/learnings/procedure.rs b/crates/terraphim_agent/src/learnings/procedure.rs index d0486c016..ad72c43a4 100644 --- a/crates/terraphim_agent/src/learnings/procedure.rs +++ b/crates/terraphim_agent/src/learnings/procedure.rs @@ -138,6 +138,7 @@ impl ProcedureStore { /// (> 0.8) exists, merge the steps instead of creating a duplicate. /// /// Returns the saved (or merged) procedure. + #[allow(dead_code)] pub fn save_with_dedup( &self, mut procedure: CapturedProcedure, @@ -377,12 +378,14 @@ impl ProcedureStore { /// /// These are navigational, informational, or read-only commands that do not /// contribute meaningful steps to a procedure. +#[allow(dead_code)] pub const TRIVIAL_COMMANDS: &[&str] = &[ "cd ", "ls", "pwd", "echo ", "cat ", "head ", "tail ", "wc ", "which ", "type ", "date", "whoami", ]; /// Check whether a command is trivial (should be excluded from procedure extraction). +#[allow(dead_code)] fn is_trivial_command(command: &str) -> bool { let trimmed = command.trim(); TRIVIAL_COMMANDS @@ -405,6 +408,7 @@ fn is_trivial_command(command: &str) -> bool { /// # Returns /// /// A `CapturedProcedure` with steps derived from the successful, non-trivial commands. +#[allow(dead_code)] pub fn from_session_commands( commands: Vec<(String, i32)>, title: Option, diff --git a/crates/terraphim_agent/src/main.rs b/crates/terraphim_agent/src/main.rs index 6f15b8980..04bdc7a72 100644 --- a/crates/terraphim_agent/src/main.rs +++ b/crates/terraphim_agent/src/main.rs @@ -61,6 +61,98 @@ enum LogicalOperatorCli { Or, } +/// Truncate a snippet at a UTF-8 char boundary, appending "..." when truncated. +/// +/// Naive `&s[..max]` panics when `max` lands inside a multi-byte char (e.g. typographic +/// quotes from email subjects). This walks char boundaries and stops at the last one +/// whose byte index is ≤ max. +fn truncate_snippet(s: &str, max_bytes: usize) -> String { + if s.len() <= max_bytes { + return s.to_string(); + } + let cutoff = s + .char_indices() + .map(|(i, _)| i) + .take_while(|&i| i <= max_bytes) + .last() + .unwrap_or(0); + format!("{}...", &s[..cutoff]) +} + +#[cfg(test)] +mod truncate_snippet_tests { + use super::truncate_snippet; + + #[test] + fn short_string_unchanged() { + assert_eq!(truncate_snippet("hello", 120), "hello"); + } + + #[test] + fn ascii_truncated() { + let s = "a".repeat(200); + let out = truncate_snippet(&s, 120); + assert!(out.ends_with("...")); + assert_eq!(out.len(), 123); + } + + #[test] + fn multibyte_does_not_panic() { + // Reproduces crates/terraphim_agent/src/main.rs:1414 panic where + // `&s[..120]` landed inside a typographic quote (3 bytes: e2 80 9c). + let s = "Includes dependencies for llama.cpp, integration with retreival, and CLI/GUI flows; the project positions itself as \u{201C}ultimate open-source RAG app\u{201D} with curated features."; + let out = truncate_snippet(s, 120); + // Must not panic and must be a valid UTF-8 string ending in "..." + assert!(out.ends_with("...")); + assert!(out.is_char_boundary(out.len())); + } + + #[test] + fn cyrillic_safe() { + let s = "консенсус ".repeat(20); + let out = truncate_snippet(&s, 120); + assert!(out.ends_with("...")); + } +} + +/// Format the one-line stderr explainability message emitted when the search +/// command auto-routes (i.e. the user did not pass `--role`). +/// +/// Exact format pinned by the design (section 5): +/// `[auto-route] picked role "" (score=, candidates=); to override, pass --role` +fn format_auto_route_line(result: &terraphim_service::auto_route::AutoRouteResult) -> String { + format!( + "[auto-route] picked role \"{}\" (score={}, candidates={}); to override, pass --role", + result.role.as_str(), + result.score, + result.candidates.len(), + ) +} + +#[cfg(test)] +mod format_auto_route_line_tests { + use super::format_auto_route_line; + use terraphim_service::auto_route::{AutoRouteReason, AutoRouteResult}; + use terraphim_types::RoleName; + + #[test] + fn pinned_exact_format() { + let r = AutoRouteResult { + role: RoleName::new("Personal Assistant"), + score: 42, + candidates: vec![ + (RoleName::new("Personal Assistant"), 42), + (RoleName::new("Default"), 0), + ], + reason: AutoRouteReason::ScoredWinner, + }; + assert_eq!( + format_auto_route_line(&r), + "[auto-route] picked role \"Personal Assistant\" (score=42, candidates=2); to override, pass --role" + ); + } +} + /// Show helpful usage information when run without a TTY fn show_usage_info() { println!("Terraphim AI Agent v{}", env!("CARGO_PKG_VERSION")); @@ -1329,7 +1421,12 @@ async fn run_offline_command( role, limit, } => { - let role_name = service.resolve_role(role.as_deref()).await?; + let (role_name, auto) = service + .resolve_or_auto_route(role.as_deref(), &query) + .await?; + if let Some(ref ar) = auto { + eprintln!("{}", format_auto_route_line(ar)); + } let results = if let Some(additional_terms) = terms { // Multi-term query with logical operators @@ -1408,14 +1505,7 @@ async fn run_offline_command( } else { Some(doc.body.as_str()) }) - .map(|s| { - let trimmed = s.trim(); - if trimmed.len() > 120 { - format!("{}...", &trimmed[..120]) - } else { - trimmed.to_string() - } - }); + .map(|s| truncate_snippet(s.trim(), 120)); println!("[{}] {}", doc.rank.unwrap_or_default(), doc.title); if !doc.url.is_empty() { println!(" {}", doc.url); @@ -2942,14 +3032,7 @@ async fn run_server_command( } else { Some(doc.body.as_str()) }) - .map(|s| { - let trimmed = s.trim(); - if trimmed.len() > 120 { - format!("{}...", &trimmed[..120]) - } else { - trimmed.to_string() - } - }); + .map(|s| truncate_snippet(s.trim(), 120)); println!("[{}] {}", doc.rank.unwrap_or_default(), doc.title); if !doc.url.is_empty() { println!(" {}", doc.url); diff --git a/crates/terraphim_agent/src/repl/commands.rs b/crates/terraphim_agent/src/repl/commands.rs index 178c7d18d..60a83d82f 100644 --- a/crates/terraphim_agent/src/repl/commands.rs +++ b/crates/terraphim_agent/src/repl/commands.rs @@ -1,5 +1,6 @@ //! Command definitions for REPL interface +use crate::robot::OutputFormat; use anyhow::{Result, anyhow}; use std::str::FromStr; @@ -12,6 +13,10 @@ pub enum ReplCommand { limit: Option, semantic: bool, concepts: bool, + /// Output format (json, jsonl, minimal, table) + format: Option, + /// Robot mode: machine-readable structured output + robot: bool, }, Config { subcommand: ConfigSubcommand, @@ -319,6 +324,8 @@ impl FromStr for ReplCommand { let mut limit = None; let _semantic = false; let _concepts = false; + let mut format: Option = None; + let mut robot = false; let mut i = 1; while i < parts.len() { @@ -343,6 +350,23 @@ impl FromStr for ReplCommand { return Err(anyhow!("--limit requires a value")); } } + "--format" => { + if i + 1 < parts.len() { + format = + Some(parts[i + 1].parse::().map_err(|e| { + anyhow!("{}\nValid formats: json, jsonl, minimal, table", e) + })?); + i += 2; + } else { + return Err(anyhow!( + "--format requires a value (json, jsonl, minimal, table)" + )); + } + } + "--robot" => { + robot = true; + i += 1; + } _ => { if !query.is_empty() { query.push(' '); @@ -379,6 +403,8 @@ impl FromStr for ReplCommand { limit, semantic, concepts, + format, + robot, }) } @@ -1490,6 +1516,8 @@ mod tests { limit: None, semantic: false, concepts: false, + format: None, + robot: false, } ); @@ -1504,6 +1532,8 @@ mod tests { limit: Some(5), semantic: false, concepts: false, + format: None, + robot: false, } ); } diff --git a/crates/terraphim_agent/src/repl/handler.rs b/crates/terraphim_agent/src/repl/handler.rs index b4a29546c..edc44da12 100644 --- a/crates/terraphim_agent/src/repl/handler.rs +++ b/crates/terraphim_agent/src/repl/handler.rs @@ -264,8 +264,19 @@ impl ReplHandler { limit, semantic, concepts, + format, + robot, } => { - self.handle_search(query, role, limit, semantic, concepts) + let robot_cfg = crate::robot::RobotConfig { + enabled: robot, + format: format.unwrap_or(if robot { + crate::robot::OutputFormat::Json + } else { + crate::robot::OutputFormat::Table + }), + ..Default::default() + }; + self.handle_search(query, role, limit, semantic, concepts, robot_cfg) .await?; } ReplCommand::Config { subcommand } => { @@ -361,7 +372,12 @@ impl ReplHandler { limit: Option, semantic: bool, concepts: bool, + robot_cfg: crate::robot::RobotConfig, ) -> Result<()> { + let use_structured = + robot_cfg.enabled || !matches!(robot_cfg.format, crate::robot::OutputFormat::Table); + let output_format = robot_cfg.format; + #[cfg(feature = "repl")] { use colored::Colorize; @@ -369,15 +385,16 @@ impl ReplHandler { use comfy_table::presets::UTF8_FULL; use comfy_table::{Cell, Table}; - let search_mode = if semantic { "semantic " } else { "" }.to_string() - + if concepts { "concepts " } else { "" }; - - println!( - "{} {}Searching for: '{}'", - "🔍".bold(), - search_mode, - query.cyan() - ); + if !use_structured { + let search_mode = if semantic { "semantic " } else { "" }.to_string() + + if concepts { "concepts " } else { "" }; + println!( + "{} {}Searching for: '{}'", + "🔍".bold(), + search_mode, + query.cyan() + ); + } if let Some(service) = &self.service { let role_name = if let Some(r) = role.as_deref() { @@ -387,7 +404,9 @@ impl ReplHandler { }; let results = service.search_with_role(&query, &role_name, limit).await?; - if results.is_empty() { + if use_structured { + Self::print_structured_results(&results, output_format)?; + } else if results.is_empty() { println!("{} No results found", "ℹ".blue().bold()); } else { let mut table = Table::new(); @@ -438,7 +457,9 @@ impl ReplHandler { match api_client.search(&search_query).await { Ok(response) => { - if response.results.is_empty() { + if use_structured { + Self::print_structured_results(&response.results, output_format)?; + } else if response.results.is_empty() { println!("{} No results found", "ℹ".blue().bold()); } else { let mut table = Table::new(); @@ -482,6 +503,38 @@ impl ReplHandler { Ok(()) } + /// Print search results in structured format (JSON/JSONL/minimal) + fn print_structured_results( + results: &[terraphim_types::Document], + format: crate::robot::OutputFormat, + ) -> Result<()> { + match format { + crate::robot::OutputFormat::Jsonl => { + for doc in results { + println!("{}", serde_json::to_string(doc)?); + } + } + crate::robot::OutputFormat::Minimal => { + let minimal: Vec = results + .iter() + .map(|d| { + serde_json::json!({ + "title": d.title, + "url": d.url, + "rank": d.rank + }) + }) + .collect(); + println!("{}", serde_json::to_string(&minimal)?); + } + _ => { + // Json and Table both produce pretty JSON in structured mode + println!("{}", serde_json::to_string_pretty(results)?); + } + } + Ok(()) + } + async fn handle_config(&self, subcommand: ConfigSubcommand) -> Result<()> { match subcommand { ConfigSubcommand::Show => { diff --git a/crates/terraphim_agent/src/service.rs b/crates/terraphim_agent/src/service.rs index 006fbcaa5..a52db1248 100644 --- a/crates/terraphim_agent/src/service.rs +++ b/crates/terraphim_agent/src/service.rs @@ -254,6 +254,59 @@ impl TuiService { } } + /// Resolve an explicit role or auto-route based on the query. + /// + /// If `role` is `Some`, behaves exactly like [`Self::resolve_role`] and returns + /// `(role_name, None)`. If `role` is `None`, scores every configured role's + /// rolegraph against `query` via + /// [`terraphim_service::auto_route::auto_select_role`] and returns + /// `(picked_role, Some(routing_result))` so callers can emit an explainability + /// line. The routing decision is never persisted. + /// + /// `selected_role` passed to `auto_select_role` is normalised: persisted + /// `selected_role` is treated as `None` when it does not exist in `config.roles`. + pub async fn resolve_or_auto_route( + &self, + role: Option<&str>, + query: &str, + ) -> Result<( + RoleName, + Option, + )> { + if let Some(r) = role { + let resolved = self + .find_role_by_name_or_shortname(r) + .await + .ok_or_else(|| anyhow::anyhow!("Role '{}' not found in config", r))?; + return Ok((resolved, None)); + } + + // Auto-route. Snapshot the live config (the helper needs Role.haystacks) + // and normalise selected_role against config.roles before passing it. + // The router scores against each role's thesaurus-driven Aho-Corasick + // automaton (built unconditionally by `RoleGraph::new_sync`), so no + // pre-warm is required -- routing works cold. The previous + // `ensure_thesaurus_loaded` loop here was redundant once #617 landed + // and is removed to avoid serialising routing behind the service mutex. + let config = self.get_config().await; + + let selected = self.get_selected_role().await; + let selected_normalised = if config.roles.contains_key(&selected) { + Some(selected) + } else { + None + }; + let ctx = terraphim_service::auto_route::AutoRouteContext::from_env(selected_normalised); + let result = terraphim_service::auto_route::auto_select_role( + query, + &config, + &self.config_state, + &ctx, + ) + .await; + Ok((result.role.clone(), Some(result))) + } + /// Search documents with a specific role pub async fn search_with_role( &self, diff --git a/crates/terraphim_agent/src/shell_dispatch.rs b/crates/terraphim_agent/src/shell_dispatch.rs index df3291f86..6a716f882 100644 --- a/crates/terraphim_agent/src/shell_dispatch.rs +++ b/crates/terraphim_agent/src/shell_dispatch.rs @@ -670,8 +670,14 @@ mod tests { #[tokio::test] async fn test_execute_dispatch_captures_exit_code() { - let config = test_config("/bin/false"); - // /bin/false ignores all args and exits 1 + // `false` exits 1. Path differs across systems: Linux ships it at + // /bin/false, macOS at /usr/bin/false. Pick whichever exists. + let false_bin = ["/usr/bin/false", "/bin/false"] + .iter() + .find(|p| std::path::Path::new(p).exists()) + .copied() + .expect("expected /usr/bin/false or /bin/false on this system"); + let config = test_config(false_bin); let result = execute_dispatch(&config, "anything", &[]).await.unwrap(); assert_ne!(result.exit_code, 0); } diff --git a/crates/terraphim_agent/tests/cli_auto_route.rs b/crates/terraphim_agent/tests/cli_auto_route.rs new file mode 100644 index 000000000..81d39dfcb --- /dev/null +++ b/crates/terraphim_agent/tests/cli_auto_route.rs @@ -0,0 +1,126 @@ +//! CLI integration tests for auto-routing (design step 4, tests T8 and T9). +//! +//! These tests shell out to `cargo run -p terraphim_agent` so they exercise the +//! same dispatch path users hit. The fixture config is the existing +//! `crates/terraphim_agent/tests/test_config.json` (a single-role config), which +//! is enough for these assertions because: +//! - T8 (explicit --role): asserts that stderr is silent on `[auto-route]`, +//! regardless of how many roles are present. +//! - T9 (--robot stdout purity): asserts that stdout parses as JSON and +//! stderr contains exactly one `[auto-route]` line. With one role the +//! routing decision is trivial, but the prefix must still appear. +//! +//! Tests scrub `RUST_LOG` and `JMAP_ACCESS_TOKEN` so dev-shell variables don't +//! poison stderr matching. Marked `#[serial]` to avoid clobbering the workspace +//! cargo build lock with peer tests in the same crate. + +use std::process::Command; + +use anyhow::{Context, Result}; +use serde_json::Value; +use serial_test::serial; + +const FIXTURE_CONFIG: &str = "tests/test_config.json"; + +fn run_agent(args: &[&str]) -> Result<(String, String, i32)> { + let output = Command::new("cargo") + .args(["run", "-p", "terraphim_agent", "--quiet", "--"]) + .args(args) + .env_remove("RUST_LOG") + .env_remove("JMAP_ACCESS_TOKEN") + .output() + .context("failed to execute terraphim-agent")?; + Ok(( + String::from_utf8_lossy(&output.stdout).to_string(), + String::from_utf8_lossy(&output.stderr).to_string(), + output.status.code().unwrap_or(-1), + )) +} + +fn count_auto_route_lines(stderr: &str) -> usize { + stderr + .lines() + .filter(|l| l.trim_start().starts_with("[auto-route]")) + .count() +} + +#[test] +#[serial] +fn t8_explicit_role_short_circuits_auto_route() -> Result<()> { + let (stdout, stderr, code) = run_agent(&[ + "--config", + FIXTURE_CONFIG, + "--robot", + "search", + "terraphim", + "--role", + "Test Engineer", + "--limit", + "1", + ])?; + + assert_eq!( + code, 0, + "explicit --role search should succeed; stderr={}", + stderr + ); + // The point of the test: no auto-route line on stderr. + assert_eq!( + count_auto_route_lines(&stderr), + 0, + "explicit --role must not emit [auto-route]; stderr={}", + stderr + ); + // Sanity: stdout still has the JSON envelope. + assert!( + stdout.contains('{'), + "expected JSON on stdout; stdout={}", + stdout + ); + Ok(()) +} + +#[test] +#[serial] +fn t9_robot_mode_stdout_is_pure_json_stderr_has_auto_route() -> Result<()> { + let (stdout, stderr, code) = run_agent(&[ + "--config", + FIXTURE_CONFIG, + "--robot", + "search", + "terraphim", + "--limit", + "1", + ])?; + + assert_eq!( + code, 0, + "auto-routed --robot search should succeed; stderr={}", + stderr + ); + + // Exactly one [auto-route] line on stderr. + assert_eq!( + count_auto_route_lines(&stderr), + 1, + "expected exactly one [auto-route] line on stderr; got:\n{}", + stderr + ); + + // Stdout must parse as JSON. Find the first `{` (skip any preamble) and + // deserialise; do not just substring-check. + let start = stdout + .find('{') + .with_context(|| format!("stdout has no JSON object; stdout={}", stdout))?; + let parsed: Value = serde_json::from_str(&stdout[start..]) + .with_context(|| format!("stdout JSON did not parse; stdout={}", stdout))?; + assert!( + parsed.get("query").is_some(), + "JSON envelope missing 'query' field" + ); + assert!( + parsed.get("role").is_some(), + "JSON envelope missing 'role' field" + ); + Ok(()) +} diff --git a/crates/terraphim_agent/tests/enhanced_search_tests.rs b/crates/terraphim_agent/tests/enhanced_search_tests.rs index 6c251f60d..ef7610820 100644 --- a/crates/terraphim_agent/tests/enhanced_search_tests.rs +++ b/crates/terraphim_agent/tests/enhanced_search_tests.rs @@ -17,6 +17,7 @@ fn test_basic_search_command_parsing() { limit, semantic, concepts, + .. } => { assert_eq!(query, "rust programming"); assert_eq!(role, None); @@ -39,6 +40,7 @@ fn test_search_with_role_command_parsing() { limit, semantic, concepts, + .. } => { assert_eq!(query, "rust programming"); assert_eq!(role, Some("Developer".to_string())); @@ -61,6 +63,7 @@ fn test_search_with_limit_command_parsing() { limit, semantic, concepts, + .. } => { assert_eq!(query, "rust programming"); assert_eq!(role, None); @@ -83,6 +86,7 @@ fn test_search_semantic_flag_parsing() { limit, semantic, concepts, + .. } => { assert_eq!(query, "rust programming"); assert_eq!(role, None); @@ -105,6 +109,7 @@ fn test_search_concepts_flag_parsing() { limit, semantic, concepts, + .. } => { assert_eq!(query, "rust programming"); assert_eq!(role, None); @@ -130,6 +135,7 @@ fn test_search_all_flags_parsing() { limit, semantic, concepts, + .. } => { assert_eq!(query, "rust programming"); assert_eq!(role, Some("Developer".to_string())); @@ -152,6 +158,7 @@ fn test_search_complex_query_parsing() { limit, semantic, concepts, + .. } => { assert_eq!(query, "\"machine learning algorithms\""); assert_eq!(role, Some("DataScientist".to_string())); @@ -201,10 +208,9 @@ fn test_search_with_multiple_words_and_spaces() { match command { ReplCommand::Search { query, - role: _, - limit: _, semantic, concepts, + .. } => { assert_eq!(query, "rust async programming"); assert!(semantic); @@ -234,6 +240,7 @@ fn test_search_flags_order_independence() { limit, semantic, concepts, + .. } => { assert_eq!(query, "test"); assert_eq!(role, Some("Dev".to_string())); @@ -259,9 +266,9 @@ fn test_search_with_special_characters() { ReplCommand::Search { query, role, - limit: _, semantic, concepts, + .. } => { assert_eq!(query, "\"C++ templates\""); assert_eq!(role, Some("CppDeveloper".to_string())); @@ -279,10 +286,9 @@ fn test_search_concepts_flag_multiple_times() { match command { ReplCommand::Search { query, - role: _, - limit: _, semantic, concepts, + .. } => { assert_eq!(query, "test"); assert!(!semantic); @@ -299,10 +305,9 @@ fn test_search_semantic_flag_multiple_times() { match command { ReplCommand::Search { query, - role: _, - limit: _, semantic, concepts, + .. } => { assert_eq!(query, "test"); assert!(semantic); // Should still be true even with multiple flags @@ -338,11 +343,7 @@ fn test_search_with_very_long_query() { match command { ReplCommand::Search { - query, - role: _, - limit: _, - semantic, - concepts: _, + query, semantic, .. } => { assert_eq!(query.len(), 1000); assert!(semantic); @@ -396,3 +397,106 @@ fn test_search_edge_cases() { panic!("Expected Search command"); } } + +/// Test --format flag parsing +#[cfg(feature = "repl")] +#[test] +fn test_search_format_json_parsing() { + use terraphim_agent::robot::OutputFormat; + let cmd = ReplCommand::from_str("/search rust --format json").unwrap(); + match cmd { + ReplCommand::Search { + query, + format, + robot, + .. + } => { + assert_eq!(query, "rust"); + assert_eq!(format, Some(OutputFormat::Json)); + assert!(!robot); + } + _ => panic!("Expected Search command"), + } +} + +/// Test --format jsonl flag parsing +#[cfg(feature = "repl")] +#[test] +fn test_search_format_jsonl_parsing() { + use terraphim_agent::robot::OutputFormat; + let cmd = ReplCommand::from_str("/search rust --format jsonl").unwrap(); + match cmd { + ReplCommand::Search { format, .. } => { + assert_eq!(format, Some(OutputFormat::Jsonl)); + } + _ => panic!("Expected Search command"), + } +} + +/// Test --format minimal flag parsing +#[cfg(feature = "repl")] +#[test] +fn test_search_format_minimal_parsing() { + use terraphim_agent::robot::OutputFormat; + let cmd = ReplCommand::from_str("/search rust --format minimal").unwrap(); + match cmd { + ReplCommand::Search { format, .. } => { + assert_eq!(format, Some(OutputFormat::Minimal)); + } + _ => panic!("Expected Search command"), + } +} + +/// Test --robot flag parsing +#[cfg(feature = "repl")] +#[test] +fn test_search_robot_flag_parsing() { + let cmd = ReplCommand::from_str("/search rust --robot").unwrap(); + match cmd { + ReplCommand::Search { robot, format, .. } => { + assert!(robot); + assert_eq!(format, None); + } + _ => panic!("Expected Search command"), + } +} + +/// Test --robot and --format together +#[cfg(feature = "repl")] +#[test] +fn test_search_robot_with_format_parsing() { + use terraphim_agent::robot::OutputFormat; + let cmd = ReplCommand::from_str("/search rust --robot --format jsonl").unwrap(); + match cmd { + ReplCommand::Search { robot, format, .. } => { + assert!(robot); + assert_eq!(format, Some(OutputFormat::Jsonl)); + } + _ => panic!("Expected Search command"), + } +} + +/// Test invalid format value returns error +#[cfg(feature = "repl")] +#[test] +fn test_search_invalid_format_returns_error() { + let result = ReplCommand::from_str("/search rust --format invalid_fmt"); + assert!(result.is_err(), "Invalid format should produce an error"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Valid formats"), + "Error should list valid formats, got: {}", + err + ); +} + +/// Test --format missing value returns error +#[cfg(feature = "repl")] +#[test] +fn test_search_format_missing_value_returns_error() { + let result = ReplCommand::from_str("/search rust --format"); + assert!( + result.is_err(), + "--format without value should produce an error" + ); +} diff --git a/crates/terraphim_agent/tests/repl_integration_tests.rs b/crates/terraphim_agent/tests/repl_integration_tests.rs index c6753cd4d..a1f04898d 100644 --- a/crates/terraphim_agent/tests/repl_integration_tests.rs +++ b/crates/terraphim_agent/tests/repl_integration_tests.rs @@ -726,6 +726,7 @@ fn test_repl_complex_search_query() { limit, semantic, concepts, + .. }) => { assert_eq!(query, "rust async programming"); assert_eq!(role, Some("engineer".to_string())); diff --git a/crates/terraphim_mcp_server/Cargo.toml b/crates/terraphim_mcp_server/Cargo.toml index 048c8cc38..09d003d17 100644 --- a/crates/terraphim_mcp_server/Cargo.toml +++ b/crates/terraphim_mcp_server/Cargo.toml @@ -37,6 +37,7 @@ axum = { version = "0.8" } [features] openrouter = ["terraphim_config/openrouter"] zlob = ["fff-search/zlob", "terraphim_file_search/zlob"] +jmap = ["terraphim_middleware/jmap"] [dev-dependencies] ahash = "0.8" diff --git a/crates/terraphim_mcp_server/src/lib.rs b/crates/terraphim_mcp_server/src/lib.rs index 8dd9a2794..3a14b171e 100644 --- a/crates/terraphim_mcp_server/src/lib.rs +++ b/crates/terraphim_mcp_server/src/lib.rs @@ -141,11 +141,30 @@ impl McpService { .await .map_err(|e| ErrorData::internal_error(e.to_string(), None))?; - // Determine which role to use (provided role or selected role) - let role_name = if let Some(role_str) = role { - RoleName::from(role_str) + // Determine which role to use. If the caller passed `role`, honour it + // verbatim (matches CLI semantics: explicit always wins). Otherwise + // auto-route on the query and prepend an explainability line to the + // response so MCP clients can see the routing decision. + let (role_name, auto) = if let Some(role_str) = role { + (RoleName::from(role_str), None) } else { - self.config_state.get_selected_role().await + let config_snapshot = self.config_state.config.lock().await.clone(); + let selected = self.config_state.get_selected_role().await; + let selected_normalised = if config_snapshot.roles.contains_key(&selected) { + Some(selected) + } else { + None + }; + let ctx = + terraphim_service::auto_route::AutoRouteContext::from_env(selected_normalised); + let result = terraphim_service::auto_route::auto_select_role( + &query, + &config_snapshot, + &self.config_state, + &ctx, + ) + .await; + (result.role.clone(), Some(result)) }; let search_query = SearchQuery { @@ -162,6 +181,14 @@ impl McpService { match service.search(&search_query).await { Ok(documents) => { let mut contents = Vec::new(); + if let Some(ref ar) = auto { + contents.push(Content::text(format!( + "[auto-route] picked role \"{}\" (score={}, candidates={}); pass role parameter to override", + ar.role.as_str(), + ar.score, + ar.candidates.len(), + ))); + } let summary = format!("Found {} documents matching your query.", documents.len()); contents.push(Content::text(summary)); diff --git a/crates/terraphim_mcp_server/tests/auto_route.rs b/crates/terraphim_mcp_server/tests/auto_route.rs new file mode 100644 index 000000000..e3d5174d8 --- /dev/null +++ b/crates/terraphim_mcp_server/tests/auto_route.rs @@ -0,0 +1,111 @@ +//! T10: MCP routing line surfaced in CallToolResult.contents. +//! +//! Constructs `McpService` directly with a hand-built `ConfigState` (one role +//! with a small KG) and calls `search` twice: +//! - With `role` argument set: response should NOT begin with `[auto-route]`. +//! - With `role` unset: response's first text content should start with +//! `[auto-route]` and subsequent contents should be unchanged. + +use ahash::AHashMap; +use std::sync::Arc; +use terraphim_config::{Config, ConfigId, ConfigState, Role}; +use terraphim_mcp_server::McpService; +use terraphim_rolegraph::{RoleGraph, RoleGraphSync}; +use terraphim_types::{ + NormalizedTerm, NormalizedTermValue, RelevanceFunction, RoleName, Thesaurus, +}; +use tokio::sync::Mutex; + +fn build_thesaurus(name: &str, terms: &[(&str, u64, &str)]) -> Thesaurus { + let mut t = Thesaurus::new(name.to_string()); + for (synonym, id, concept) in terms { + t.insert( + NormalizedTermValue::from(*synonym), + NormalizedTerm::new(*id, NormalizedTermValue::from(*concept)), + ); + } + t +} + +async fn fixture() -> Arc { + let role_name = RoleName::new("Test Engineer"); + + // Build a tiny rolegraph with one matched term. + let thesaurus = build_thesaurus("test", &[("rfp", 1, "rfp")]); + let rg = RoleGraph::new(role_name.clone(), thesaurus).await.unwrap(); + let rg_sync = RoleGraphSync::from(rg); + + // Use TitleScorer so service.search() does not require a KG configured on + // disk; auto-routing still works because it only inspects the in-memory + // rolegraphs we put in `state.roles`. + let mut role = Role::new(role_name.clone()); + role.relevance_function = RelevanceFunction::TitleScorer; + + let mut roles_map = AHashMap::new(); + roles_map.insert(role_name.clone(), role); + let mut rg_map = AHashMap::new(); + rg_map.insert(role_name.clone(), rg_sync); + + let config = Config { + id: ConfigId::Embedded, + global_shortcut: "Ctrl+X".to_string(), + roles: roles_map, + default_role: role_name.clone(), + selected_role: role_name.clone(), + }; + + Arc::new(ConfigState { + config: Arc::new(Mutex::new(config)), + roles: rg_map, + }) +} + +fn first_text(result: &rmcp::model::CallToolResult) -> Option { + use rmcp::model::RawContent; + result.content.first().and_then(|c| match &c.raw { + RawContent::Text(t) => Some(t.text.clone()), + _ => None, + }) +} + +#[tokio::test] +async fn t10_auto_route_line_prepended_when_role_omitted() { + let state = fixture().await; + let service = McpService::new(state); + + let with_role = service + .search( + "rfp".to_string(), + Some("Test Engineer".to_string()), + Some(5), + None, + ) + .await + .expect("search with explicit role should not error"); + + let without_role = service + .search("rfp".to_string(), None, Some(5), None) + .await + .expect("search without role should not error"); + + let auto_text = first_text(&without_role).expect("expected text content first"); + assert!( + auto_text.starts_with("[auto-route]"), + "expected first content to start with [auto-route]; got: {auto_text}" + ); + + // Explicit-role response should NOT lead with [auto-route]. + let explicit_first = first_text(&with_role).expect("expected text content first"); + assert!( + !explicit_first.starts_with("[auto-route]"), + "explicit role should not emit [auto-route]; got: {explicit_first}" + ); + + // Auto-routed response should have exactly one extra leading text item + // compared to the explicit-role response. + assert_eq!( + without_role.content.len(), + with_role.content.len() + 1, + "auto-routed response should have one extra (leading) content item" + ); +} diff --git a/crates/terraphim_mcp_server/tests/integration_test.rs b/crates/terraphim_mcp_server/tests/integration_test.rs index bd66cb520..fb6be683a 100644 --- a/crates/terraphim_mcp_server/tests/integration_test.rs +++ b/crates/terraphim_mcp_server/tests/integration_test.rs @@ -189,6 +189,7 @@ async fn test_mcp_server_integration() -> Result<()> { name: "search".into(), arguments: serde_json::json!({ "query": query, + "role": "Default", "limit": 5 }) .as_object() @@ -424,12 +425,15 @@ async fn test_search_pagination() -> Result<()> { }) .await?; - // First page + // First page. Pass `role` explicitly so the auto-router does not prepend + // the `[auto-route]` text content -- this test asserts pagination shape, + // not routing behaviour. let first_page = service .call_tool(CallToolRequestParam { name: "search".into(), arguments: serde_json::json!({ "query": "terraphim", + "role": "Default", "limit": 2 }) .as_object() @@ -445,12 +449,13 @@ async fn test_search_pagination() -> Result<()> { .filter(|c| c.as_resource().is_some()) .count(); - // Second page (skip=2) + // Second page (skip=2). Same explicit-role short-circuit as above. let second_page = service .call_tool(CallToolRequestParam { name: "search".into(), arguments: serde_json::json!({ "query": "terraphim", + "role": "Default", "limit": 2, "skip": 2 }) diff --git a/crates/terraphim_mcp_server/tests/mcp_rolegraph_validation_test.rs b/crates/terraphim_mcp_server/tests/mcp_rolegraph_validation_test.rs index ccff448ce..be5469014 100644 --- a/crates/terraphim_mcp_server/tests/mcp_rolegraph_validation_test.rs +++ b/crates/terraphim_mcp_server/tests/mcp_rolegraph_validation_test.rs @@ -210,7 +210,11 @@ async fn test_mcp_server_terraphim_engineer_search() -> Result<()> { } // Check if we got results - let result_count = search_result.content.len().saturating_sub(1); // Subtract summary message + let result_count = search_result + .content + .iter() + .filter(|c| c.as_resource().is_some()) + .count(); // Subtract summary message println!("Found {} documents for '{}'", result_count, query); // Print detailed search result for debugging @@ -303,7 +307,11 @@ async fn test_mcp_role_switching_before_search() -> Result<()> { }) .await?; - let default_results = default_search.content.len().saturating_sub(1); + let default_results = default_search + .content + .iter() + .filter(|c| c.as_resource().is_some()) + .count(); println!( "Default config found {} results for 'terraphim-graph'", default_results @@ -343,7 +351,11 @@ async fn test_mcp_role_switching_before_search() -> Result<()> { }) .await?; - let updated_results = updated_search.content.len().saturating_sub(1); + let updated_results = updated_search + .content + .iter() + .filter(|c| c.as_resource().is_some()) + .count(); println!( "Terraphim Engineer config found {} results for 'terraphim-graph'", updated_results @@ -433,7 +445,11 @@ async fn test_mcp_resource_operations() -> Result<()> { }) .await?; - let search_results = test_search.content.len().saturating_sub(1); + let search_results = test_search + .content + .iter() + .filter(|c| c.as_resource().is_some()) + .count(); println!( "Regular search found {} results for 'terraphim-graph'", search_results @@ -655,7 +671,15 @@ async fn test_mcp_search_uses_selected_role() -> Result<()> { !search_without_role.is_error.unwrap_or(false), "Search without role should succeed" ); - let results_without_role = search_without_role.content.len().saturating_sub(1); + // Count only resource contents -- the no-role path now also prepends an + // `[auto-route]` text content alongside the existing heading, so the + // pre-auto-route `len - 1` arithmetic is no longer valid. Counting + // resources directly is robust to additional text prepends. + let results_without_role = search_without_role + .content + .iter() + .filter(|c| c.as_resource().is_some()) + .count(); println!( "Search WITHOUT role parameter found {} results", results_without_role @@ -680,7 +704,11 @@ async fn test_mcp_search_uses_selected_role() -> Result<()> { !search_with_role.is_error.unwrap_or(false), "Search with role should succeed" ); - let results_with_role = search_with_role.content.len().saturating_sub(1); + let results_with_role = search_with_role + .content + .iter() + .filter(|c| c.as_resource().is_some()) + .count(); println!( "Search WITH role parameter found {} results", results_with_role @@ -718,7 +746,11 @@ async fn test_mcp_search_uses_selected_role() -> Result<()> { // This might fail if Default role doesn't exist, but that's okay - we're testing the override mechanism if !search_different_role.is_error.unwrap_or(false) { - let results_different_role = search_different_role.content.len().saturating_sub(1); + let results_different_role = search_different_role + .content + .iter() + .filter(|c| c.as_resource().is_some()) + .count(); println!( "Search with different role found {} results", results_different_role @@ -750,7 +782,11 @@ async fn test_mcp_search_uses_selected_role() -> Result<()> { !graph_search.is_error.unwrap_or(false), "Graph search should succeed" ); - let graph_results = graph_search.content.len().saturating_sub(1); + let graph_results = graph_search + .content + .iter() + .filter(|c| c.as_resource().is_some()) + .count(); println!("Search for 'graph' found {} results", graph_results); service.cancel().await?; diff --git a/crates/terraphim_middleware/Cargo.toml b/crates/terraphim_middleware/Cargo.toml index bc2f4c71b..d986f6f7e 100644 --- a/crates/terraphim_middleware/Cargo.toml +++ b/crates/terraphim_middleware/Cargo.toml @@ -18,8 +18,7 @@ terraphim_rolegraph = { path = "../terraphim_rolegraph", version = "1.0.0" } terraphim_automata = { path = "../terraphim_automata", version = "1.0.0", features = ["tokio-runtime"] } terraphim_types = { path = "../terraphim_types", version = "1.0.0" } terraphim_persistence = { path = "../terraphim_persistence", version = "1.0.0" } -# NOTE: haystack_jmap not published to crates.io yet -# haystack_jmap = { path = "../haystack_jmap", version = "1.0.0", optional = true } +haystack_jmap = { path = "../haystack_jmap", version = "1.0.0", optional = true } # NOTE: Atomic Data Server commented out for crates.io publishing (not published yet) # terraphim_atomic_client = { path = "../terraphim_atomic_client", version = "1.0.0", features = ["native"], optional = true } grepapp_haystack = { path = "../haystack_grepapp", version = "1.0.0", optional = true } @@ -76,8 +75,7 @@ default = [] # atomic = ["dep:terraphim_atomic_client"] atomic = [] grepapp = ["dep:grepapp_haystack"] -# NOTE: jmap feature disabled - haystack_jmap not published to crates.io yet -jmap = [] +jmap = ["dep:haystack_jmap"] # Enable AI coding assistant session haystack (Claude Code, OpenCode, Cursor, Aider, Codex) ai-assistant = ["terraphim-session-analyzer", "jiff", "home"] # Enable openrouter integration diff --git a/crates/terraphim_middleware/tests/jmap_haystack.rs b/crates/terraphim_middleware/tests/jmap_haystack.rs new file mode 100644 index 000000000..233cddb28 --- /dev/null +++ b/crates/terraphim_middleware/tests/jmap_haystack.rs @@ -0,0 +1,53 @@ +//! T11: JMAP regression -- the historic UTF-8-panic query against the PA's +//! JMAP haystack must still return >=50 results without panicking. +//! +//! Gated on `--features jmap` (compile-time) and `$JMAP_ACCESS_TOKEN` being set +//! at runtime. When the token is absent the test exits early with a `println!` +//! so cargo still records it as ok; CI without credentials should not fail. +//! +//! Invocation: +//! JMAP_ACCESS_TOKEN=$(op read "op://...token...") \ +//! cargo test -p terraphim_middleware --test jmap_haystack \ +//! --features jmap -- --ignored --nocapture +//! +//! The `--ignored` gate prevents accidental network calls during normal +//! `cargo test --workspace` runs. + +#![cfg(feature = "jmap")] + +use terraphim_config::{Haystack, ServiceType}; +use terraphim_middleware::haystack::JmapHaystackIndexer; +use terraphim_middleware::indexer::IndexMiddleware; + +#[tokio::test] +#[ignore = "requires JMAP_ACCESS_TOKEN and live Fastmail connectivity"] +async fn t11_pa_terraphim_query_returns_50_plus_jmap_results() { + let token = match std::env::var("JMAP_ACCESS_TOKEN") { + Ok(t) if !t.trim().is_empty() => t, + _ => { + println!("JMAP_ACCESS_TOKEN unset; skipping T11"); + return; + } + }; + + // Build a minimal Jmap haystack pointing at Fastmail's session URL. The + // indexer reads `JMAP_ACCESS_TOKEN` from the env directly. + let _ = token; // env is consumed by the indexer + let haystack = Haystack::new( + "https://api.fastmail.com/jmap/session".to_string(), + ServiceType::Jmap, + true, + ); + + let indexer = JmapHaystackIndexer; + let index = indexer + .index("terraphim", &haystack) + .await + .expect("JMAP index call should not error"); + + let count = index.get_all_documents().len(); + assert!( + count >= 50, + "PA JMAP query for 'terraphim' returned {count} results; expected >=50" + ); +} diff --git a/crates/terraphim_orchestrator/Cargo.toml b/crates/terraphim_orchestrator/Cargo.toml index cd81455e4..7b5bb0d36 100644 --- a/crates/terraphim_orchestrator/Cargo.toml +++ b/crates/terraphim_orchestrator/Cargo.toml @@ -45,6 +45,7 @@ terraphim_automata = { path = "../terraphim_automata", version = "1.4.10" } # Config parsing toml = "0.8" +glob = "0.3" # Template rendering handlebars = "6.3" diff --git a/crates/terraphim_orchestrator/orchestrator.example.toml b/crates/terraphim_orchestrator/orchestrator.example.toml index f5b52e60d..ce2b007cf 100644 --- a/crates/terraphim_orchestrator/orchestrator.example.toml +++ b/crates/terraphim_orchestrator/orchestrator.example.toml @@ -217,6 +217,70 @@ kind = "shell" script = """terraphim-agent --config /opt/ai-dark-factory/persona_roles_config.json search "bounded context architecture decision API design" --role "Carthos Architecture" --limit 5 2>/dev/null || echo '[]'""" timeout_secs = 30 +# ----------------------------------------------------------------------------- +# PROJECT-META (per-project coordinator). One instance per project; dispatches +# the next unblocked Gitea issue to the appropriate role. Uses env variables +# injected by the orchestrator at spawn time ($ADF_PROJECT_ID, $ADF_WORKING_DIR, +# $GITEA_OWNER, $GITEA_REPO) so the same prompt works across all projects. +# ----------------------------------------------------------------------------- + +[[agents]] +name = "project-meta" +layer = "Core" +cli_tool = "claude" +persona = "carthos" +terraphim_role = "Carthos Architecture" +skill_chain = ["disciplined-research", "disciplined-verification"] +schedule = "*/15 * * * *" +# NOTE: set `project = ""` per-project override, or duplicate this agent +# as `project-meta-` to run one instance per project. +capabilities = ["coordination", "dispatch", "project-health"] +task = """You are the per-project coordinator for `$ADF_PROJECT_ID`. + +Working dir: `$ADF_WORKING_DIR` +Gitea target: `$GITEA_OWNER/$GITEA_REPO` + +## Step 1 -- health check the project +Run from `$ADF_WORKING_DIR`: + cd "$ADF_WORKING_DIR" + git status --porcelain | head -20 + cargo check --workspace 2>&1 | tail -20 + +## Step 2 -- find the next unblocked issue +Use the gitea-robot CLI with $GITEA_OWNER/$GITEA_REPO: + gtr ready --owner "$GITEA_OWNER" --repo "$GITEA_REPO" + +Pick the single highest-PageRank unblocked issue. Do NOT dispatch more than one. + +## Step 3 -- route to the right role +Inspect the issue title, body, and labels to choose one of the standard roles: +- implementation-swarm (default for feature/bugfix) +- test-guardian (issues tagged quality/testing) +- security-sentinel (issues tagged security/*) +- spec-validator (issues tagged spec/*) +- documentation-generator (issues tagged docs/*) + +Post a claiming comment on the issue using gtr: + gtr comment --owner "$GITEA_OWNER" --repo "$GITEA_REPO" --issue \\ + --body "project-meta ($ADF_PROJECT_ID) dispatching @adf: for this issue" + +## Step 4 -- emit a journal event so the fleet-meta can tally work +Print exactly this line to stdout as the last line of your output: + project_meta: project=$ADF_PROJECT_ID dispatched= issue= + +If nothing is unblocked, print: + project_meta: project=$ADF_PROJECT_ID dispatched=none issue=none + +## Domain Knowledge Search +When you need architecture context, use: + terraphim-agent --config /opt/ai-dark-factory/persona_roles_config.json search "" --role "Carthos Architecture" +""" + +[agents.pre_check] +kind = "shell" +script = """terraphim-agent --config /opt/ai-dark-factory/persona_roles_config.json search "coordination dispatch project ready queue" --role "Carthos Architecture" --limit 5 2>/dev/null || echo '[]'""" +timeout_secs = 30 + [[agents]] name = "spec-validator" layer = "Core" @@ -458,12 +522,101 @@ kind = "shell" script = """terraphim-agent --config /opt/ai-dark-factory/persona_roles_config.json search "knowledge graph learning pattern decision framework" --role "Mneme Knowledge" --limit 5 2>/dev/null || echo '[]'""" timeout_secs = 30 +# ============================================================================= +# FLEET-META (global, cron-only). Shell-script agent that reports fleet health, +# subscription budget, and per-project status to a daily coordination report +# and opens [ADF] Gitea issues for threshold breaches. No mentions. +# ============================================================================= + +[[agents]] +name = "fleet-meta" +layer = "Growth" +cli_tool = "/bin/bash" +schedule = "0 * * * *" +capabilities = ["fleet-health", "budget", "coordination-report"] +task = """ +set -u + +DATE=$(date +%Y-%m-%d) +HOUR=$(date +%H) +REPORT_DIR="/opt/ai-dark-factory/workspace/reports" +REPORT="${REPORT_DIR}/coordination-${DATE}.md" +BUDGET_DIR="/opt/ai-dark-factory/data/budget" +PAUSE_DIR="/opt/ai-dark-factory/data/pause" +OWNER="${GITEA_OWNER:-terraphim}" +REPO="${GITEA_REPO:-adf-fleet}" + +mkdir -p "${REPORT_DIR}" + +{ + echo "# ADF coordination report -- ${DATE} ${HOUR}:00" + echo + echo "## Fleet health" + echo + echo "### Recent service journal (last hour)" + echo '```' + journalctl -u adf-orchestrator.service --since "1 hour ago" --no-pager 2>/dev/null | tail -40 || echo "journalctl unavailable" + echo '```' + echo + echo "### Orchestrator processes" + echo '```' + ps -o pid,etime,rss,cmd -C terraphim_orchestrator 2>/dev/null || echo "no orchestrator process running" + echo '```' + echo + echo "### Disk" + echo '```' + df -h /opt/ai-dark-factory 2>/dev/null || df -h / + echo '```' + echo + echo "### Memory" + echo '```' + free -h + echo '```' + echo + echo "## Subscription budgets" + echo + if [ -d "${BUDGET_DIR}" ]; then + for f in "${BUDGET_DIR}"/*.json; do + [ -e "$f" ] || continue + echo "### $(basename "$f")" + echo '```json' + cat "$f" + echo '```' + echo + done + else + echo "_no budget directory_" + fi + echo + echo "## Paused projects" + if [ -d "${PAUSE_DIR}" ]; then + ls -1 "${PAUSE_DIR}" 2>/dev/null | sed 's/^/ - /' || echo " (none)" + else + echo " (pause directory absent)" + fi + echo +} > "${REPORT}" + +echo "wrote ${REPORT}" + +# Threshold-breach escalation: if any paused project exists, open an [ADF] issue. +if [ -d "${PAUSE_DIR}" ] && [ -n "$(ls -A "${PAUSE_DIR}" 2>/dev/null)" ]; then + PAUSED=$(ls -1 "${PAUSE_DIR}" | tr '\\n' ' ') + TITLE="[ADF] paused projects detected: ${PAUSED}" + BODY=$(printf 'One or more projects are paused by the circuit breaker.\\n\\nPaused: %s\\n\\nSee: %s\\n' "${PAUSED}" "${REPORT}") + gtr create-issue --owner "${OWNER}" --repo "${REPO}" \\ + --title "${TITLE}" --body "${BODY}" --labels "adf,fleet-meta,priority/high" \\ + 2>&1 || echo "gtr create-issue failed (continuing)" +fi +""" + # ============================================================================= # FLOW DAG -- Compound Review v2 # ============================================================================= [[flows]] name = "compound-review-v2" +project = "default" schedule = "0 2 * * *" repo_path = "/opt/ai-dark-factory/workspace" base_branch = "main" diff --git a/crates/terraphim_orchestrator/src/bin/adf.rs b/crates/terraphim_orchestrator/src/bin/adf.rs index ae781400a..9c4ccd01d 100644 --- a/crates/terraphim_orchestrator/src/bin/adf.rs +++ b/crates/terraphim_orchestrator/src/bin/adf.rs @@ -1,5 +1,7 @@ use std::path::PathBuf; +use std::process::ExitCode; +use terraphim_orchestrator::config::OrchestratorConfig; use terraphim_orchestrator::AgentOrchestrator; use terraphim_types::capability::{Capability, CostLevel, Latency, Provider, ProviderType}; use tracing_subscriber::EnvFilter; @@ -104,58 +106,236 @@ fn register_providers(orchestrator: &mut AgentOrchestrator) { tracing::info!("registered 4 LLM providers for keyword routing"); } +enum Cli { + Run { config: PathBuf }, + Check { config: PathBuf }, + Help, +} + +fn parse_args() -> Result { + let args: Vec = std::env::args().skip(1).collect(); + let mut check: Option = None; + let mut positional: Option = None; + + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + match arg.as_str() { + "--check" => { + let path = iter + .next() + .ok_or_else(|| "--check requires a config path".to_string())?; + check = Some(PathBuf::from(path)); + } + "-h" | "--help" => return Ok(Cli::Help), + other if other.starts_with("--") => { + return Err(format!("unknown flag: {other}")); + } + other => { + if positional.is_some() { + return Err(format!("unexpected positional argument: {other}")); + } + positional = Some(PathBuf::from(other)); + } + } + } + + if let Some(config) = check { + Ok(Cli::Check { config }) + } else { + let config = + positional.unwrap_or_else(|| PathBuf::from("/opt/ai-dark-factory/orchestrator.toml")); + Ok(Cli::Run { config }) + } +} + +fn print_help() { + println!("adf -- AI Dark Factory orchestrator"); + println!(); + println!("USAGE:"); + println!(" adf [CONFIG] Run the orchestrator"); + println!(" adf --check CONFIG Validate config + print agent routing table"); + println!(" adf --help Show this message"); +} + +/// Run the dry-run validator: load, validate, and print the routing table. +/// Returns exit code 0 on success, 1 on failure. +fn run_check(path: PathBuf) -> ExitCode { + let config = match OrchestratorConfig::from_file(&path) { + Ok(c) => c, + Err(e) => { + eprintln!("adf --check FAILED to load {}: {e}", path.display()); + return ExitCode::from(1); + } + }; + + if let Err(e) = config.validate() { + eprintln!("adf --check FAILED validation for {}: {e}", path.display()); + return ExitCode::from(1); + } + + print_routing_table(&config); + ExitCode::SUCCESS +} + +/// Print a sorted table of `(project_id, agent_name, model_or_fallback, layer)`. +fn print_routing_table(config: &OrchestratorConfig) { + // Build rows: (project, agent, model_or_fallback, layer) + let mut rows: Vec<(String, String, String, String)> = Vec::with_capacity(config.agents.len()); + for agent in &config.agents { + let project = agent + .project + .clone() + .unwrap_or_else(|| "".to_string()); + let model = match (&agent.model, &agent.fallback_model) { + (Some(m), _) => m.clone(), + (None, Some(fb)) => format!("(fallback) {fb}"), + (None, None) => "".to_string(), + }; + rows.push(( + project, + agent.name.clone(), + model, + format!("{:?}", agent.layer), + )); + } + rows.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))); + + let w_project = rows + .iter() + .map(|r| r.0.len()) + .chain(std::iter::once("PROJECT".len())) + .max() + .unwrap_or(7); + let w_agent = rows + .iter() + .map(|r| r.1.len()) + .chain(std::iter::once("AGENT".len())) + .max() + .unwrap_or(5); + let w_model = rows + .iter() + .map(|r| r.2.len()) + .chain(std::iter::once("MODEL".len())) + .max() + .unwrap_or(5); + let w_layer = rows + .iter() + .map(|r| r.3.len()) + .chain(std::iter::once("LAYER".len())) + .max() + .unwrap_or(5); + + println!( + "{: Result<(), Box> { +async fn main() -> ExitCode { tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), ) .init(); - let config_path = std::env::args() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/opt/ai-dark-factory/orchestrator.toml")); - - tracing::info!(config = %config_path.display(), "loading orchestrator config"); - - let mut orchestrator = AgentOrchestrator::from_config_file(&config_path)?; - - // Register LLM providers for keyword-based model selection - register_providers(&mut orchestrator); - - #[cfg(feature = "quickwit")] - { - if let Some(qw_config) = orchestrator.quickwit_config().cloned() { - if qw_config.enabled { - let sink = terraphim_orchestrator::quickwit::QuickwitSink::new( - qw_config.endpoint.clone(), - qw_config.index_id.clone(), - qw_config.batch_size, - qw_config.flush_interval_secs, - ); - orchestrator.set_quickwit_sink(sink); - tracing::info!( - endpoint = %qw_config.endpoint, - index = %qw_config.index_id, - "Quickwit logging enabled" - ); - } + let cli = match parse_args() { + Ok(cli) => cli, + Err(e) => { + eprintln!("error: {e}"); + print_help(); + return ExitCode::from(2); } - } + }; - // Handle SIGTERM/SIGINT for graceful shutdown - let shutdown_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); - let flag = shutdown_flag.clone(); + match cli { + Cli::Help => { + print_help(); + ExitCode::SUCCESS + } + Cli::Check { config } => run_check(config), + Cli::Run { config } => { + tracing::info!(config = %config.display(), "loading orchestrator config"); - tokio::spawn(async move { - tokio::signal::ctrl_c().await.ok(); - tracing::info!("received shutdown signal"); - flag.store(true, std::sync::atomic::Ordering::SeqCst); - }); + let mut orchestrator = match AgentOrchestrator::from_config_file(&config) { + Ok(o) => o, + Err(e) => { + eprintln!("failed to load config {}: {e}", config.display()); + return ExitCode::from(1); + } + }; - tracing::info!("starting AI Dark Factory orchestrator"); - orchestrator.run().await?; + register_providers(&mut orchestrator); - Ok(()) + #[cfg(feature = "quickwit")] + { + use terraphim_orchestrator::quickwit::{QuickwitFleetSink, QuickwitSink}; + + let fleet_configs = orchestrator.quickwit_fleet_configs(); + if !fleet_configs.is_empty() { + let mut fleet = QuickwitFleetSink::new_multi(); + for (project_id, qw_config) in fleet_configs { + let sink = QuickwitSink::new( + qw_config.endpoint.clone(), + qw_config.index_id.clone(), + qw_config.batch_size, + qw_config.flush_interval_secs, + ); + tracing::info!( + project = %project_id, + endpoint = %qw_config.endpoint, + index = %qw_config.index_id, + "Quickwit logging enabled for project" + ); + fleet.insert_project(project_id, sink); + } + orchestrator.set_quickwit_sink(fleet); + } + } + + let shutdown_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let flag = shutdown_flag.clone(); + + tokio::spawn(async move { + tokio::signal::ctrl_c().await.ok(); + tracing::info!("received shutdown signal"); + flag.store(true, std::sync::atomic::Ordering::SeqCst); + }); + + tracing::info!("starting AI Dark Factory orchestrator"); + if let Err(e) = orchestrator.run().await { + eprintln!("orchestrator error: {e}"); + return ExitCode::from(1); + } + + ExitCode::SUCCESS + } + } } diff --git a/crates/terraphim_orchestrator/src/concurrency.rs b/crates/terraphim_orchestrator/src/concurrency.rs index 3fed74bf2..10c505159 100644 --- a/crates/terraphim_orchestrator/src/concurrency.rs +++ b/crates/terraphim_orchestrator/src/concurrency.rs @@ -1,8 +1,9 @@ //! Global concurrency controller with fairness. //! -//! Enforces global agent limits and ensures fairness between time-driven -//! and issue-driven modes. +//! Enforces global agent limits, per-mode quotas, and per-project caps, and +//! ensures fairness between time-driven and issue-driven modes. +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{Mutex, Semaphore}; @@ -13,7 +14,10 @@ pub struct ConcurrencyController { global: Arc, /// Per-mode quotas. quotas: ModeQuotas, - /// Currently running agents by mode. + /// Optional per-project caps (project id -> max concurrent agents). + /// Missing entries mean "no per-project cap". + project_caps: Arc>, + /// Currently running agents by mode + per-project. running: Arc>, /// Fairness policy. fairness: FairnessPolicy, @@ -28,11 +32,29 @@ pub struct ModeQuotas { pub issue_max: usize, } +/// Per-project concurrency caps. +#[derive(Debug, Clone, Copy)] +pub struct ProjectCaps { + /// Maximum concurrent agents (time + issue + mention) for this project. + pub max_concurrent_agents: usize, + /// Maximum concurrent mention-driven agents for this project. + /// None means "no per-project mention cap" (fall back to global). + pub max_concurrent_mention_agents: Option, +} + /// Currently running agent counts. #[derive(Debug, Default)] struct RunningCounts { time_driven: usize, issue_driven: usize, + mention_driven: usize, + per_project: HashMap, +} + +#[derive(Debug, Default, Clone, Copy)] +struct ProjectRunning { + total: usize, + mention: usize, } /// Fairness policy for mode coordination. @@ -63,6 +85,7 @@ impl std::str::FromStr for FairnessPolicy { pub struct AgentPermit { _global: tokio::sync::OwnedSemaphorePermit, mode: AgentMode, + project: String, running: Arc>, } @@ -70,49 +93,91 @@ pub struct AgentPermit { enum AgentMode { TimeDriven, IssueDriven, + MentionDriven, } impl Drop for AgentPermit { fn drop(&mut self) { let mode = self.mode; + let project = std::mem::take(&mut self.project); let running = self.running.clone(); tokio::spawn(async move { let mut counts = running.lock().await; match mode { - AgentMode::TimeDriven => counts.time_driven -= 1, - AgentMode::IssueDriven => counts.issue_driven -= 1, + AgentMode::TimeDriven => counts.time_driven = counts.time_driven.saturating_sub(1), + AgentMode::IssueDriven => { + counts.issue_driven = counts.issue_driven.saturating_sub(1) + } + AgentMode::MentionDriven => { + counts.mention_driven = counts.mention_driven.saturating_sub(1) + } + } + if let Some(proj) = counts.per_project.get_mut(&project) { + proj.total = proj.total.saturating_sub(1); + if let AgentMode::MentionDriven = mode { + proj.mention = proj.mention.saturating_sub(1); + } + if proj.total == 0 && proj.mention == 0 { + counts.per_project.remove(&project); + } } }); } } impl ConcurrencyController { - /// Create a new concurrency controller. + /// Create a new concurrency controller with no per-project caps. pub fn new(global_max: usize, quotas: ModeQuotas, fairness: FairnessPolicy) -> Self { + Self::with_project_caps(global_max, quotas, fairness, HashMap::new()) + } + + /// Create a new concurrency controller with explicit per-project caps. + pub fn with_project_caps( + global_max: usize, + quotas: ModeQuotas, + fairness: FairnessPolicy, + project_caps: HashMap, + ) -> Self { Self { global: Arc::new(Semaphore::new(global_max)), quotas, + project_caps: Arc::new(project_caps), running: Arc::new(Mutex::new(RunningCounts::default())), fairness, } } - /// Try to acquire a slot for a time-driven agent. - pub async fn acquire_time_driven(&self) -> Option { - self.acquire(AgentMode::TimeDriven).await + /// Try to acquire a slot for a time-driven agent in the given project. + pub async fn acquire_time_driven(&self, project: &str) -> Option { + self.acquire(AgentMode::TimeDriven, project).await } - /// Try to acquire a slot for an issue-driven agent. - pub async fn acquire_issue_driven(&self) -> Option { - self.acquire(AgentMode::IssueDriven).await + /// Try to acquire a slot for an issue-driven agent in the given project. + pub async fn acquire_issue_driven(&self, project: &str) -> Option { + self.acquire(AgentMode::IssueDriven, project).await } - /// Get current running counts. + /// Try to acquire a slot for a mention-driven agent in the given project. + pub async fn acquire_mention_driven(&self, project: &str) -> Option { + self.acquire(AgentMode::MentionDriven, project).await + } + + /// Get current running counts (time_driven, issue_driven). pub async fn running_counts(&self) -> (usize, usize) { let counts = self.running.lock().await; (counts.time_driven, counts.issue_driven) } + /// Get the running count for a specific project. + pub async fn project_running_count(&self, project: &str) -> usize { + let counts = self.running.lock().await; + counts + .per_project + .get(project) + .map(|p| p.total) + .unwrap_or(0) + } + /// Get available slots. pub fn available_slots(&self) -> usize { self.global.available_permits() @@ -124,22 +189,63 @@ impl ConcurrencyController { match mode { AgentMode::TimeDriven => counts.time_driven < self.quotas.time_max, AgentMode::IssueDriven => counts.issue_driven < self.quotas.issue_max, + // Mention-driven currently has no global mode quota; per-project + // mention cap is checked separately in `project_has_capacity`. + AgentMode::MentionDriven => true, } } + /// Check if project has capacity for this mode. + async fn project_has_capacity(&self, mode: AgentMode, project: &str) -> bool { + let Some(caps) = self.project_caps.get(project) else { + return true; + }; + let counts = self.running.lock().await; + let running = counts.per_project.get(project).copied().unwrap_or_default(); + + if running.total >= caps.max_concurrent_agents { + tracing::debug!( + project, + total = running.total, + cap = caps.max_concurrent_agents, + "per-project cap reached" + ); + return false; + } + if matches!(mode, AgentMode::MentionDriven) { + if let Some(mention_cap) = caps.max_concurrent_mention_agents { + if running.mention >= mention_cap { + tracing::debug!( + project, + mention = running.mention, + cap = mention_cap, + "per-project mention cap reached" + ); + return false; + } + } + } + true + } + /// Get the active fairness policy. pub fn fairness_policy(&self) -> FairnessPolicy { self.fairness } - /// Acquire a slot for the given mode. - async fn acquire(&self, mode: AgentMode) -> Option { + /// Acquire a slot for the given mode in the given project. + async fn acquire(&self, mode: AgentMode, project: &str) -> Option { // Check mode quota first if !self.mode_has_capacity(mode).await { tracing::debug!(?mode, "mode quota exceeded"); return None; } + // Check per-project cap + if !self.project_has_capacity(mode, project).await { + return None; + } + // Apply fairness policy: under Proportional, check whether the mode // is consuming more than its fair share of global capacity. if self.fairness == FairnessPolicy::Proportional { @@ -147,20 +253,31 @@ impl ConcurrencyController { let total = counts.time_driven + counts.issue_driven; let global_cap = self.global.available_permits() + total; if global_cap > 0 { + // Proportional fairness only applies to time/issue modes with + // quotas; mention-driven has no mode quota and is exempt. let mode_count = match mode { AgentMode::TimeDriven => counts.time_driven, AgentMode::IssueDriven => counts.issue_driven, + AgentMode::MentionDriven => 0, }; let mode_quota = match mode { AgentMode::TimeDriven => self.quotas.time_max, AgentMode::IssueDriven => self.quotas.issue_max, + AgentMode::MentionDriven => usize::MAX, }; let total_quota = self.quotas.time_max + self.quotas.issue_max; // Fair share = global_cap * (mode_quota / total_quota) - let fair_share = (global_cap * mode_quota) / total_quota.max(1); - if mode_count >= fair_share && fair_share > 0 { - tracing::debug!(?mode, mode_count, fair_share, "proportional fairness limit"); - return None; + if !matches!(mode, AgentMode::MentionDriven) { + let fair_share = (global_cap * mode_quota) / total_quota.max(1); + if mode_count >= fair_share && fair_share > 0 { + tracing::debug!( + ?mode, + mode_count, + fair_share, + "proportional fairness limit" + ); + return None; + } } } } @@ -180,14 +297,21 @@ impl ConcurrencyController { match mode { AgentMode::TimeDriven => counts.time_driven += 1, AgentMode::IssueDriven => counts.issue_driven += 1, + AgentMode::MentionDriven => counts.mention_driven += 1, + } + let entry = counts.per_project.entry(project.to_string()).or_default(); + entry.total += 1; + if matches!(mode, AgentMode::MentionDriven) { + entry.mention += 1; } } - tracing::debug!(?mode, "acquired concurrency slot"); + tracing::debug!(?mode, project, "acquired concurrency slot"); Some(AgentPermit { _global: global_permit, mode, + project: project.to_string(), running: self.running.clone(), }) } @@ -206,6 +330,8 @@ impl Default for ModeQuotas { mod tests { use super::*; + const TEST_PROJECT: &str = "__global__"; + #[tokio::test] async fn test_acquire_release() { let controller = ConcurrencyController::new( @@ -218,15 +344,15 @@ mod tests { ); // Acquire first permit - let permit1 = controller.acquire_time_driven().await; + let permit1 = controller.acquire_time_driven(TEST_PROJECT).await; assert!(permit1.is_some()); // Acquire second permit - let permit2 = controller.acquire_time_driven().await; + let permit2 = controller.acquire_time_driven(TEST_PROJECT).await; assert!(permit2.is_some()); // Third should fail (global limit) - let permit3 = controller.acquire_time_driven().await; + let permit3 = controller.acquire_time_driven(TEST_PROJECT).await; assert!(permit3.is_none()); // Drop one and try again @@ -235,7 +361,7 @@ mod tests { // Wait a bit for the drop to propagate tokio::time::sleep(std::time::Duration::from_millis(10)).await; - let permit4 = controller.acquire_time_driven().await; + let permit4 = controller.acquire_time_driven(TEST_PROJECT).await; assert!(permit4.is_some()); } @@ -251,19 +377,19 @@ mod tests { ); // Acquire time-driven slot - let time_permit = controller.acquire_time_driven().await; + let time_permit = controller.acquire_time_driven(TEST_PROJECT).await; assert!(time_permit.is_some()); // Second time-driven should fail - let time_permit2 = controller.acquire_time_driven().await; + let time_permit2 = controller.acquire_time_driven(TEST_PROJECT).await; assert!(time_permit2.is_none()); // But issue-driven should succeed - let issue_permit = controller.acquire_issue_driven().await; + let issue_permit = controller.acquire_issue_driven(TEST_PROJECT).await; assert!(issue_permit.is_some()); // Second issue-driven should fail - let issue_permit2 = controller.acquire_issue_driven().await; + let issue_permit2 = controller.acquire_issue_driven(TEST_PROJECT).await; assert!(issue_permit2.is_none()); } @@ -278,14 +404,100 @@ mod tests { FairnessPolicy::RoundRobin, ); - let _time_permit = controller.acquire_time_driven().await.unwrap(); - let _issue_permit = controller.acquire_issue_driven().await.unwrap(); + let _time_permit = controller.acquire_time_driven(TEST_PROJECT).await.unwrap(); + let _issue_permit = controller.acquire_issue_driven(TEST_PROJECT).await.unwrap(); let (time_count, issue_count) = controller.running_counts().await; assert_eq!(time_count, 1); assert_eq!(issue_count, 1); } + #[tokio::test] + async fn test_per_project_cap_saturates_independently() { + let mut caps = HashMap::new(); + caps.insert( + "alpha".to_string(), + ProjectCaps { + max_concurrent_agents: 1, + max_concurrent_mention_agents: None, + }, + ); + caps.insert( + "beta".to_string(), + ProjectCaps { + max_concurrent_agents: 2, + max_concurrent_mention_agents: None, + }, + ); + let controller = ConcurrencyController::with_project_caps( + 10, + ModeQuotas { + time_max: 5, + issue_max: 5, + }, + FairnessPolicy::RoundRobin, + caps, + ); + + // alpha: cap 1 + let a1 = controller.acquire_time_driven("alpha").await; + assert!(a1.is_some()); + let a2 = controller.acquire_time_driven("alpha").await; + assert!( + a2.is_none(), + "alpha should be saturated after reaching its cap of 1" + ); + + // beta: cap 2 — independent of alpha + let b1 = controller.acquire_time_driven("beta").await; + let b2 = controller.acquire_time_driven("beta").await; + assert!(b1.is_some()); + assert!(b2.is_some()); + let b3 = controller.acquire_time_driven("beta").await; + assert!( + b3.is_none(), + "beta should be saturated after reaching its cap of 2" + ); + + // Release alpha — permit slot returns to alpha independently. + drop(a1); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + let a3 = controller.acquire_time_driven("alpha").await; + assert!(a3.is_some(), "alpha should have capacity after drop"); + } + + #[tokio::test] + async fn test_per_project_mention_cap() { + let mut caps = HashMap::new(); + caps.insert( + "alpha".to_string(), + ProjectCaps { + max_concurrent_agents: 5, + max_concurrent_mention_agents: Some(1), + }, + ); + let controller = ConcurrencyController::with_project_caps( + 10, + ModeQuotas { + time_max: 5, + issue_max: 5, + }, + FairnessPolicy::RoundRobin, + caps, + ); + + let m1 = controller.acquire_mention_driven("alpha").await; + assert!(m1.is_some()); + let m2 = controller.acquire_mention_driven("alpha").await; + assert!( + m2.is_none(), + "mention cap of 1 should block second mention acquire" + ); + // Other modes still have capacity under the total cap of 5. + let t1 = controller.acquire_time_driven("alpha").await; + assert!(t1.is_some()); + } + #[test] fn test_fairness_policy_from_str() { assert_eq!( diff --git a/crates/terraphim_orchestrator/src/config.rs b/crates/terraphim_orchestrator/src/config.rs index fbb9aa242..05d0718b2 100644 --- a/crates/terraphim_orchestrator/src/config.rs +++ b/crates/terraphim_orchestrator/src/config.rs @@ -29,6 +29,44 @@ fn default_pre_check_timeout() -> u64 { 60 } +/// Definition of a single project within a multi-project fleet. +/// +/// Each project carries its own working directory, Gitea target, mention +/// rate caps, workflow tracker, and Quickwit index. Agents and flows are +/// bound to exactly one project via `project` fields; the orchestrator +/// routes per-project configuration to each agent at dispatch time. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + /// Unique project id (e.g. "odilo", "digital-twins", "terraphim"). + pub id: String, + /// Per-project working directory (overrides top-level working_dir for this project's agents). + pub working_dir: PathBuf, + /// Minutes offset added to cron schedules of agents in this project (stagger the fleet). + #[serde(default)] + pub schedule_offset_minutes: u8, + /// Per-project Gitea output config (owner/repo). + #[serde(default)] + pub gitea: Option, + /// Per-project mention config (rate caps). + #[serde(default)] + pub mentions: Option, + /// Per-project workflow / tracker config. + #[serde(default)] + pub workflow: Option, + /// Per-project Quickwit index config. + #[cfg(feature = "quickwit")] + #[serde(default)] + pub quickwit: Option, + /// Maximum concurrent agents (time + issue + mention) for this project. + /// Unset = no per-project cap beyond the global concurrency controller. + #[serde(default)] + pub max_concurrent_agents: Option, + /// Maximum concurrent mention-driven agents for this project. + /// Unset = fall back to global mention cap only. + #[serde(default)] + pub max_concurrent_mention_agents: Option, +} + /// Top-level orchestrator configuration (parsed from TOML). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrchestratorConfig { @@ -42,6 +80,7 @@ pub struct OrchestratorConfig { #[serde(default)] pub workflow: Option, /// Agent definitions. + #[serde(default)] pub agents: Vec, /// Seconds to wait before restarting a Safety agent after it exits. #[serde(default = "default_restart_cooldown")] @@ -95,6 +134,49 @@ pub struct OrchestratorConfig { #[cfg(feature = "quickwit")] #[serde(default)] pub quickwit: Option, + /// Multi-project definitions. Empty means single-project legacy mode (globals are used). + #[serde(default)] + pub projects: Vec, + /// Include globs. Each matching file is merged into this config + /// (appends to projects / agents / flows). Globs are expanded relative + /// to the base config file's parent directory. + #[serde(default)] + pub include: Vec, + /// Per-provider spend caps for hour / day tumbling UTC windows. + /// Empty means no provider-level budget gating. + #[serde(default)] + pub providers: Vec, + /// Optional path for persisting ProviderBudgetTracker state across + /// restarts. Ignored when `providers` is empty. + #[serde(default)] + pub provider_budget_state_file: Option, + /// Directory containing per-project pause flag files. + /// + /// When a file named `/` exists, the orchestrator + /// skips dispatching any agent belonging to that project. Operators + /// remove the file to resume dispatches. The project circuit breaker + /// creates entries here automatically after repeated `project-meta` + /// failures. + /// + /// Default: [`crate::project_control::DEFAULT_PAUSE_DIR`]. + #[serde(default)] + pub pause_dir: Option, + /// Number of consecutive `project-meta` failures required before the + /// orchestrator pauses dispatch for the affected project and opens an + /// `[ADF]` escalation issue. + /// + /// Default: [`crate::project_control::DEFAULT_PROJECT_CIRCUIT_BREAKER_THRESHOLD`]. + #[serde(default = "default_project_circuit_breaker_threshold")] + pub project_circuit_breaker_threshold: u32, + /// Owner of the Gitea repo where the project circuit breaker opens + /// escalation issues (e.g. `terraphim`). When `None`, the orchestrator + /// falls back to [`GiteaOutputConfig::owner`] if configured. + #[serde(default)] + pub fleet_escalation_owner: Option, + /// Repo name for fleet-level escalation issues (e.g. `adf-fleet`). When + /// `None`, the orchestrator falls back to [`GiteaOutputConfig::repo`]. + #[serde(default)] + pub fleet_escalation_repo: Option, } /// Configuration for KG-driven model routing. @@ -172,6 +254,16 @@ fn default_max_concurrent_mention_agents() -> u32 { 5 } +impl Default for MentionConfig { + fn default() -> Self { + Self { + poll_modulo: default_poll_modulo(), + max_dispatches_per_tick: default_max_dispatches_per_tick(), + max_concurrent_mention_agents: default_max_concurrent_mention_agents(), + } + } +} + /// Configuration for the webhook server. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebhookConfig { @@ -314,6 +406,13 @@ pub struct AgentDefinition { /// Gitea issue number to post output to (optional). #[serde(default)] pub gitea_issue: Option, + + /// Project this agent belongs to. Must match a `Project.id` when any + /// projects are defined. `None` means the agent is global / legacy + /// single-project mode; mixing per-project and global agents is + /// rejected at load time. + #[serde(default)] + pub project: Option, } /// Agent layer in the dark factory hierarchy. @@ -625,18 +724,240 @@ fn default_tick_interval() -> u64 { 30 } +fn default_project_circuit_breaker_threshold() -> u32 { + crate::project_control::DEFAULT_PROJECT_CIRCUIT_BREAKER_THRESHOLD +} + +/// Partial config parsed from `include`d files. Only project definitions, +/// agents, and flows are merged in; all top-level globals are ignored. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct IncludeFragment { + #[serde(default)] + projects: Vec, + #[serde(default)] + agents: Vec, + #[serde(default)] + flows: Vec, +} + +/// Subscription-only LLM providers allowed by constraint C1. +/// Any `model` or `fallback_model` string with a `/`-prefixed provider +/// name must appear in this list; bare names (`sonnet`, `opus`) are +/// interpreted as claude-code CLI targets and always allowed. +pub const ALLOWED_PROVIDER_PREFIXES: &[&str] = &[ + "claude-code", + "opencode-go", + "kimi-for-coding", + "minimax-coding-plan", + "zai-coding-plan", +]; + +/// Explicitly banned provider prefixes. Anything matching these is rejected +/// at load time so a misconfigured fleet never reaches a pay-per-use +/// provider at runtime. Note `minimax/` is banned but the +/// `minimax-coding-plan/` subscription variant remains allowed. +pub const BANNED_PROVIDER_PREFIXES: &[&str] = &[ + "opencode", + "github-copilot", + "google", + "huggingface", + "minimax", +]; + +/// Bare model names routed through claude-code CLI (no explicit provider prefix). +pub const CLAUDE_CLI_BARE_MODELS: &[&str] = &["sonnet", "opus", "haiku"]; + +/// Anthropic-branded bare models that map onto the claude-code CLI. +pub const ANTHROPIC_BARE_PROVIDERS: &[&str] = &["anthropic"]; + +/// Runtime check: is this model's provider prefix in the allowed subscription +/// set? Returns `true` for known bare names (routed through claude-code CLI +/// or explicitly listed in [`ALLOWED_PROVIDER_PREFIXES`]) and for strings +/// whose `/`-delimited prefix appears in [`ALLOWED_PROVIDER_PREFIXES`]. +/// Anthropic-branded bare models also pass. +/// +/// This is an explicit allow-list: unknown bare names (including bare banned +/// ids like `"minimax"`) are rejected. Matches prefixes by exact equality -- +/// `opencode-go` is allowed, `opencode` is banned, `minimax-coding-plan` is +/// allowed, `minimax` is banned. +/// +/// Accepts either the full `provider/model` string (e.g. `kimi-for-coding/k2p5`) +/// or a bare provider id (e.g. `opencode-go`). +pub fn is_allowed_provider(provider_or_model: &str) -> bool { + // `/`-prefixed form: check the prefix against the explicit allow-list. + if let Some((prefix, _)) = provider_or_model.split_once('/') { + if ANTHROPIC_BARE_PROVIDERS.contains(&prefix) { + return true; + } + // Exact prefix match. Banned prefixes take precedence so + // `minimax/` is rejected even though `minimax-coding-plan/` is allowed. + if BANNED_PROVIDER_PREFIXES.contains(&prefix) { + return false; + } + return ALLOWED_PROVIDER_PREFIXES.contains(&prefix); + } + + // Bare name (no slash): explicit allow-list only. Previously this + // branch fell through to `true` for any unknown id, which meant a + // bare banned id (e.g. `model = "minimax"`) silently passed. Now + // every bare name must appear in one of the known-safe lists. + CLAUDE_CLI_BARE_MODELS.contains(&provider_or_model) + || ANTHROPIC_BARE_PROVIDERS.contains(&provider_or_model) + || ALLOWED_PROVIDER_PREFIXES.contains(&provider_or_model) +} + +/// Validate that a `model` / `fallback_model` string routes through an +/// allowed subscription provider. Returns `Ok(())` for allowed strings, +/// `Err(OrchestratorError::BannedProvider)` for banned ones. +pub(crate) fn validate_model_provider( + agent_name: &str, + field: &str, + model: &str, +) -> Result<(), crate::error::OrchestratorError> { + // Bare names like "sonnet", "opus", "haiku" -> claude-code CLI. + if !model.contains('/') { + if CLAUDE_CLI_BARE_MODELS.contains(&model) + || ANTHROPIC_BARE_PROVIDERS.contains(&model) + || ALLOWED_PROVIDER_PREFIXES.contains(&model) + { + return Ok(()); + } + // Unknown bare name: treat as banned. A bare id like `"minimax"` + // must not slip through just because the operator omitted the + // slash; operators have to opt in via an explicit allow-list entry. + return Err(crate::error::OrchestratorError::BannedProvider { + agent: agent_name.to_string(), + provider: model.to_string(), + field: field.to_string(), + }); + } + + let prefix = model.split('/').next().unwrap_or(""); + + if ANTHROPIC_BARE_PROVIDERS.contains(&prefix) { + return Ok(()); + } + + for banned in BANNED_PROVIDER_PREFIXES { + if prefix == *banned { + return Err(crate::error::OrchestratorError::BannedProvider { + agent: agent_name.to_string(), + provider: model.to_string(), + field: field.to_string(), + }); + } + } + + if ALLOWED_PROVIDER_PREFIXES.contains(&prefix) { + return Ok(()); + } + + // Unknown prefix: treat as banned so the fleet cannot silently route + // to a provider the operator has not approved. + Err(crate::error::OrchestratorError::BannedProvider { + agent: agent_name.to_string(), + provider: model.to_string(), + field: field.to_string(), + }) +} + impl OrchestratorConfig { - /// Parse an OrchestratorConfig from a TOML string. + /// Find a project definition by id. + pub fn project_by_id(&self, id: &str) -> Option<&Project> { + self.projects.iter().find(|p| p.id == id) + } + + /// Resolve the effective working directory for an agent: the project's + /// `working_dir` if the agent has a `project` and it matches a known + /// project, else the top-level working_dir. + pub fn working_dir_for_agent(&self, agent: &AgentDefinition) -> PathBuf { + agent + .project + .as_deref() + .and_then(|pid| self.project_by_id(pid)) + .map(|p| p.working_dir.clone()) + .unwrap_or_else(|| self.working_dir.clone()) + } + + /// Parse an OrchestratorConfig from a TOML string. Does not expand + /// `include` globs; use `from_file` when include expansion is needed. pub fn from_toml(toml_str: &str) -> Result { toml::from_str(toml_str).map_err(|e| crate::error::OrchestratorError::Config(e.to_string())) } - /// Load an OrchestratorConfig from a TOML file. + /// Load an OrchestratorConfig from a TOML file, expanding any + /// `include = [...]` globs relative to the base file's parent dir. + /// Each include file is parsed as an `IncludeFragment` (projects / + /// agents / flows only) and appended onto the base config. + /// + /// Validation (project id uniqueness, project refs, banned providers, + /// mixed mode) runs after merging -- use `validate()` to trigger it. pub fn from_file( path: impl AsRef, ) -> Result { - let content = std::fs::read_to_string(path.as_ref())?; - Self::from_toml(&content) + let path = path.as_ref(); + let content = std::fs::read_to_string(path)?; + let mut config = Self::from_toml(&content)?; + + if config.include.is_empty() { + return Ok(config); + } + + let base_dir = path.parent().unwrap_or_else(|| std::path::Path::new(".")); + let patterns = std::mem::take(&mut config.include); + + for pattern in &patterns { + let full_pattern = if std::path::Path::new(pattern).is_absolute() { + pattern.clone() + } else { + base_dir.join(pattern).to_string_lossy().into_owned() + }; + + let entries = glob::glob(&full_pattern).map_err(|e| { + crate::error::OrchestratorError::InvalidIncludeGlob { + pattern: pattern.clone(), + reason: e.to_string(), + } + })?; + + let mut matched: Vec = entries + .filter_map(|r| r.ok()) + .filter(|p| p != path) + .collect(); + matched.sort(); + + for include_path in matched { + let include_content = std::fs::read_to_string(&include_path)?; + let fragment: IncludeFragment = toml::from_str(&include_content).map_err(|e| { + crate::error::OrchestratorError::Config(format!( + "failed to parse include file '{}': {e}", + include_path.display() + )) + })?; + config.projects.extend(fragment.projects); + config.agents.extend(fragment.agents); + config.flows.extend(fragment.flows); + } + } + + // Preserve the original include patterns so downstream tools can + // show what was merged. + config.include = patterns; + + Ok(config) + } + + /// Load from a TOML file, expand include globs, and validate. + /// + /// Single entry-point for production startup and `adf --check`. Callers + /// that need a pre-parsed config can call `from_file` + `validate` directly. + pub fn load_and_validate( + path: impl AsRef, + ) -> Result { + let cfg = Self::from_file(path)?; + cfg.validate()?; + Ok(cfg) } /// Substitute environment variables in workflow config. @@ -648,6 +969,10 @@ impl OrchestratorConfig { } /// Validate the configuration. + /// + /// Runs all load-time checks: workflow requirements, pre-check strategy + /// dependencies, duplicate project ids, agent/flow project references, + /// banned LLM providers (C1), and mixed single/multi-project mode. pub fn validate(&self) -> Result<(), crate::error::OrchestratorError> { // Validate workflow config if present if let Some(ref workflow) = self.workflow { @@ -677,6 +1002,71 @@ impl OrchestratorConfig { } } + // Validate project ids are unique. + let mut seen_ids: std::collections::HashSet<&str> = std::collections::HashSet::new(); + for project in &self.projects { + if !seen_ids.insert(project.id.as_str()) { + return Err(crate::error::OrchestratorError::DuplicateProjectId( + project.id.clone(), + )); + } + } + + let multi_project = !self.projects.is_empty(); + + // Every agent.project (if Some) must reference a known project id. + // In multi-project mode, every agent must set project; in legacy + // mode agent.project must be None. + for agent in &self.agents { + match (&agent.project, multi_project) { + (Some(pid), _) => { + if !seen_ids.contains(pid.as_str()) { + return Err(crate::error::OrchestratorError::UnknownAgentProject { + agent: agent.name.clone(), + project: pid.clone(), + }); + } + } + (None, true) => { + return Err(crate::error::OrchestratorError::MixedProjectMode { + kind: "agent", + name: agent.name.clone(), + }); + } + (None, false) => {} + } + } + + // Every flow.project must reference a known project id. + // Flows are always per-project (D14); if projects is empty but a + // flow has a project string, treat that as an unresolved reference + // so operators see a clear error instead of a silent orphan. + for flow in &self.flows { + if !multi_project { + // Empty projects list but flows exist -> mixed mode error. + return Err(crate::error::OrchestratorError::MixedProjectMode { + kind: "flow", + name: flow.name.clone(), + }); + } + if !seen_ids.contains(flow.project.as_str()) { + return Err(crate::error::OrchestratorError::UnknownFlowProject { + flow: flow.name.clone(), + project: flow.project.clone(), + }); + } + } + + // C1: banned subscription providers. + for agent in &self.agents { + if let Some(model) = &agent.model { + validate_model_provider(&agent.name, "model", model)?; + } + if let Some(model) = &agent.fallback_model { + validate_model_provider(&agent.name, "fallback_model", model)?; + } + } + Ok(()) } } @@ -897,7 +1287,7 @@ task = "t" let example_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("orchestrator.example.toml"); let config = OrchestratorConfig::from_file(&example_path).unwrap(); - assert_eq!(config.agents.len(), 16); + assert_eq!(config.agents.len(), 18); assert_eq!(config.agents[0].layer, AgentLayer::Safety); assert_eq!(config.agents[1].layer, AgentLayer::Safety); assert_eq!(config.agents[2].layer, AgentLayer::Core); @@ -1549,6 +1939,7 @@ task = "t" [[flows]] name = "test-flow" +project = "default" repo_path = "/home/user/project" [[flows.steps]] @@ -1617,4 +2008,91 @@ task = "t" assert!(config.flows.is_empty()); assert!(config.flow_state_dir.is_none()); } + + // --- is_allowed_provider: C1/C3 runtime gate --- + + #[test] + fn test_is_allowed_provider_c1_allowed_prefixes() { + // Every prefix in ALLOWED_PROVIDER_PREFIXES must pass in `provider/model` form. + assert!(is_allowed_provider("claude-code/sonnet-4.5")); + assert!(is_allowed_provider("opencode-go/kimi-k2.5")); + assert!(is_allowed_provider("kimi-for-coding/k2p5")); + assert!(is_allowed_provider("minimax-coding-plan/MiniMax-M2.5")); + assert!(is_allowed_provider("zai-coding-plan/glm-4.6")); + } + + #[test] + fn test_is_allowed_provider_c3_banned_prefixes() { + // Every prefix in BANNED_PROVIDER_PREFIXES must be rejected. + assert!(!is_allowed_provider("opencode/whatever")); + assert!(!is_allowed_provider("github-copilot/gpt-4.1")); + assert!(!is_allowed_provider("google/gemini-2.5")); + assert!(!is_allowed_provider("huggingface/llama-3")); + assert!(!is_allowed_provider("minimax/MiniMax-M2.5")); + } + + #[test] + fn test_is_allowed_provider_c3_prefix_boundary() { + // Exact prefix match: banned `opencode` must not shadow allowed + // `opencode-go`; banned `minimax` must not shadow `minimax-coding-plan`. + assert!(is_allowed_provider("opencode-go/any")); + assert!(!is_allowed_provider("opencode/any")); + assert!(is_allowed_provider("minimax-coding-plan/any")); + assert!(!is_allowed_provider("minimax/any")); + } + + #[test] + fn test_is_allowed_provider_bare_claude_cli() { + // Bare model names route through claude-code CLI and pass. + assert!(is_allowed_provider("sonnet")); + assert!(is_allowed_provider("opus")); + assert!(is_allowed_provider("haiku")); + assert!(is_allowed_provider("anthropic")); + } + + #[test] + fn test_is_allowed_provider_bare_allowed_id() { + // Bare provider ids that appear in the allow-list pass. + assert!(is_allowed_provider("claude-code")); + assert!(is_allowed_provider("opencode-go")); + assert!(is_allowed_provider("kimi-for-coding")); + } + + #[test] + fn test_is_allowed_provider_anthropic_prefixed() { + // `anthropic/` routes through claude-code CLI and must pass. + assert!(is_allowed_provider("anthropic/claude-3.5-sonnet")); + assert!(is_allowed_provider("anthropic/claude-opus-4")); + } + + #[test] + fn test_is_allowed_provider_rejects_unknown_bare_names() { + // P1 fix: bare banned ids must now reject. Previously any bare + // string was waved through, so `model = "minimax"` silently + // bypassed the C3 banlist. + assert!(!is_allowed_provider("minimax")); + assert!(!is_allowed_provider("opencode")); + assert!(!is_allowed_provider("google")); + assert!(!is_allowed_provider("github-copilot")); + assert!(!is_allowed_provider("huggingface")); + // And any other unknown bare id. + assert!(!is_allowed_provider("unknown")); + assert!(!is_allowed_provider("")); + } + + #[test] + fn test_validate_model_provider_rejects_bare_banned() { + // The load-time check must also refuse bare banned ids -- the + // runtime filter alone cannot catch them if the validator waves + // the config through at startup. + let err = validate_model_provider("bare-banned", "model", "minimax") + .expect_err("bare 'minimax' must be rejected"); + assert!(matches!( + err, + crate::error::OrchestratorError::BannedProvider { .. } + )); + // And the allowed bare forms still pass. + validate_model_provider("ok", "model", "sonnet").expect("sonnet is a bare claude CLI"); + validate_model_provider("ok", "model", "anthropic").expect("anthropic bare passes"); + } } diff --git a/crates/terraphim_orchestrator/src/control_plane/events.rs b/crates/terraphim_orchestrator/src/control_plane/events.rs index 161dfdb37..b9d4ef0a2 100644 --- a/crates/terraphim_orchestrator/src/control_plane/events.rs +++ b/crates/terraphim_orchestrator/src/control_plane/events.rs @@ -231,6 +231,7 @@ pub fn normalize_webhook_dispatch( issue_number, comment_id, context, + .. } => { let event_id = generate_event_id(&ctx.repo_full_name, *issue_number, *comment_id); let session_id = generate_session_id(&ctx.repo_full_name, *issue_number); @@ -380,6 +381,7 @@ mod tests { let webhook_dispatch = WebhookDispatch::SpawnAgent { agent_name: "security-sentinel".to_string(), + detected_project: None, issue_number: 42, comment_id: 12345, context: "review this".to_string(), @@ -517,6 +519,7 @@ mod tests { let dispatch = WebhookDispatch::SpawnAgent { agent_name: "security-sentinel".to_string(), + detected_project: None, issue_number: 42, comment_id: 12345, context: "check for vulnerabilities".to_string(), @@ -554,6 +557,7 @@ mod tests { let dispatch = WebhookDispatch::SpawnAgent { agent_name: "agent1".to_string(), + detected_project: None, issue_number: 42, comment_id: 123, context: "do something".to_string(), @@ -588,6 +592,7 @@ mod tests { let dispatch1 = WebhookDispatch::SpawnAgent { agent_name: "agent1".to_string(), + detected_project: None, issue_number: 42, comment_id: 123, context: "do something".to_string(), @@ -595,6 +600,7 @@ mod tests { let dispatch2 = WebhookDispatch::SpawnAgent { agent_name: "agent1".to_string(), + detected_project: None, issue_number: 42, comment_id: 124, // Different comment context: "do something".to_string(), @@ -631,6 +637,7 @@ mod tests { let dispatch1 = WebhookDispatch::SpawnAgent { agent_name: "agent1".to_string(), + detected_project: None, issue_number: 42, comment_id: 123, context: "do something".to_string(), @@ -638,6 +645,7 @@ mod tests { let dispatch2 = WebhookDispatch::SpawnAgent { agent_name: "agent1".to_string(), + detected_project: None, issue_number: 43, // Different issue comment_id: 123, context: "do something".to_string(), @@ -674,6 +682,7 @@ mod tests { let webhook_dispatch = WebhookDispatch::SpawnAgent { agent_name: "security-sentinel".to_string(), + detected_project: None, issue_number: 42, comment_id: 12345, context: "review".to_string(), diff --git a/crates/terraphim_orchestrator/src/control_plane/routing.rs b/crates/terraphim_orchestrator/src/control_plane/routing.rs index 3fbc34dd2..10781e020 100644 --- a/crates/terraphim_orchestrator/src/control_plane/routing.rs +++ b/crates/terraphim_orchestrator/src/control_plane/routing.rs @@ -8,6 +8,7 @@ use crate::control_plane::telemetry::TelemetryStore; use crate::cost_tracker::BudgetVerdict; use crate::kg_router::KgRouter; +use crate::provider_budget::{provider_key_for_model, ProviderBudgetTracker}; use std::path::PathBuf; use std::sync::Arc; use terraphim_types::capability::{CostLevel, Latency, Provider, ProviderType}; @@ -130,6 +131,10 @@ pub struct RoutingDecisionEngine { unhealthy_providers: Vec, router: terraphim_router::Router, telemetry_store: Option>, + /// Per-provider hour/day budget tracker. When present, candidates + /// whose provider verdict is `Exhausted` are stripped before scoring + /// and `NearExhaustion` entries are deprioritised. + provider_budget: Option>, } impl RoutingDecisionEngine { @@ -138,12 +143,29 @@ impl RoutingDecisionEngine { unhealthy_providers: Vec, router: terraphim_router::Router, telemetry_store: Option>, + ) -> Self { + Self::with_provider_budget( + kg_router, + unhealthy_providers, + router, + telemetry_store, + None, + ) + } + + pub fn with_provider_budget( + kg_router: Option>, + unhealthy_providers: Vec, + router: terraphim_router::Router, + telemetry_store: Option>, + provider_budget: Option>, ) -> Self { Self { kg_router, unhealthy_providers, router, telemetry_store, + provider_budget, } } @@ -324,6 +346,70 @@ impl RoutingDecisionEngine { all_candidates.push(static_cand.clone()); } + // Defence-in-depth: strip any candidate whose model prefix is not an + // allowed subscription provider. Load-time `validate()` already enforces + // C1/C3 but a malformed KG or telemetry store could still surface a + // banned target at runtime. Drop it before scoring so the engine + // cannot select a pay-per-use provider. + let before_filter = all_candidates.len(); + all_candidates.retain(|cand| { + let allowed = crate::config::is_allowed_provider(&cand.model); + if !allowed { + tracing::warn!( + agent = %ctx.agent_name, + provider = %cand.provider.name, + model = %cand.model, + source = ?cand.source, + "routing: dropped banned candidate (C1/C3 gate)" + ); + } + allowed + }); + let filtered_out = before_filter - all_candidates.len(); + + // Per-provider budget gate: drop candidates whose hourly or daily + // spend has exhausted its configured cap. `NearExhaustion` does + // not drop the candidate but is factored into the score below. + let mut budget_exhausted_keys: Vec = Vec::new(); + let mut near_exhaustion_keys: Vec = Vec::new(); + if let Some(tracker) = self.provider_budget.as_ref() { + let before_budget = all_candidates.len(); + all_candidates.retain(|cand| { + let Some(key) = provider_key_for_model(&cand.model) else { + return true; + }; + match tracker.check(key) { + BudgetVerdict::Exhausted { .. } => { + if !budget_exhausted_keys.iter().any(|k| k == key) { + budget_exhausted_keys.push(key.to_string()); + } + tracing::warn!( + agent = %ctx.agent_name, + provider_key = %key, + model = %cand.model, + "routing: dropped provider-budget-exhausted candidate" + ); + false + } + BudgetVerdict::NearExhaustion { .. } => { + if !near_exhaustion_keys.iter().any(|k| k == key) { + near_exhaustion_keys.push(key.to_string()); + } + true + } + _ => true, + } + }); + let budget_dropped = before_budget - all_candidates.len(); + if budget_dropped > 0 { + tracing::info!( + agent = %ctx.agent_name, + dropped = budget_dropped, + "routing: stripped provider-budget-exhausted candidates" + ); + } + } + if all_candidates.is_empty() { let candidate = RouteCandidate { provider: make_agent_provider(&ctx.agent_name, &ctx.cli_tool), @@ -332,12 +418,26 @@ impl RoutingDecisionEngine { source: RouteSource::CliDefault, confidence: 0.5, }; - return RoutingDecision { - candidate: candidate.clone(), - rationale: format!( + let rationale = if filtered_out > 0 { + format!( + "All {} candidate(s) filtered out by C1/C3 allow-list; using CLI default ({})", + filtered_out, cli_name + ) + } else if !budget_exhausted_keys.is_empty() { + format!( + "All candidates dropped by provider-budget gate ({}); using CLI default ({})", + budget_exhausted_keys.join(","), + cli_name + ) + } else { + format!( "No routing signal matched; using CLI default ({})", cli_name - ), + ) + }; + return RoutingDecision { + candidate: candidate.clone(), + rationale, all_candidates: vec![candidate], primary_available: false, dominant_signal: RouteSource::CliDefault, @@ -355,6 +455,21 @@ impl RoutingDecisionEngine { .map(|c| Self::score_candidate(c, pressure)) .collect(); + // Provider-budget near-exhaustion deprioritisation. Multiply the + // score by 0.6 so a candidate whose provider is 80%+ into its + // quota is still eligible but loses to a healthy alternative. + let mut provider_budget_influenced = false; + if !near_exhaustion_keys.is_empty() { + for (i, cand) in all_candidates.iter().enumerate() { + if let Some(key) = provider_key_for_model(&cand.model) { + if near_exhaustion_keys.iter().any(|k| k == key) { + pressured_scores[i] *= 0.6; + provider_budget_influenced = true; + } + } + } + } + // Apply telemetry-based scoring adjustments let mut telemetry_influenced = false; if let Some(ref store) = self.telemetry_store { @@ -395,12 +510,13 @@ impl RoutingDecisionEngine { let winner = &all_candidates[winner_idx]; let dominant_signal = winner.source.clone(); - let budget_influenced = pressure != BudgetPressure::NoPressure + let agent_budget_influenced = pressure != BudgetPressure::NoPressure && no_pressure_scores.iter().enumerate().any(|(i, &s)| { let was_winner = s >= no_pressure_scores[winner_idx]; let now_loses = pressured_scores[i] < pressured_scores[winner_idx]; was_winner && now_loses }); + let budget_influenced = agent_budget_influenced || provider_budget_influenced; let mut rationale_parts = Vec::new(); @@ -429,9 +545,15 @@ impl RoutingDecisionEngine { signal_summary, ); - if budget_influenced { + if agent_budget_influenced { rationale.push_str(". Budget pressure biased selection toward cheaper model"); } + if provider_budget_influenced { + rationale.push_str(&format!( + ". Provider near-exhaustion deprioritised: {}", + near_exhaustion_keys.join(","), + )); + } if telemetry_influenced { rationale.push_str(". Telemetry data influenced selection"); } @@ -520,15 +642,14 @@ mod tests { #[tokio::test] async fn test_static_model_selected_when_only_signal() { let engine = test_engine(); - let ctx = create_test_context_with_static_model( - "test-agent", - "Implement a feature", - "claude-3-opus", - ); + // Use an allow-list-conformant bare model -- `sonnet` is a + // claude-code CLI alias and passes C1/C3. + let ctx = + create_test_context_with_static_model("test-agent", "Implement a feature", "sonnet"); let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; assert_eq!(decision.candidate.source, RouteSource::StaticConfig); - assert_eq!(decision.candidate.model, "claude-3-opus"); + assert_eq!(decision.candidate.model, "sonnet"); assert!(decision.rationale.contains("static config")); assert_eq!(decision.dominant_signal, RouteSource::StaticConfig); } @@ -578,11 +699,12 @@ mod tests { #[tokio::test] async fn test_rationale_records_dominant_signal() { let engine = test_engine(); - let ctx = create_test_context_with_static_model("agent", "task", "model-x"); + // `opus` is an allow-list-conformant claude-code CLI alias. + let ctx = create_test_context_with_static_model("agent", "task", "opus"); let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; assert!(decision.rationale.contains("static config")); - assert!(decision.rationale.contains("Selected model-x")); + assert!(decision.rationale.contains("Selected opus")); } #[tokio::test] @@ -591,7 +713,8 @@ mod tests { let ctx = DispatchContext { agent_name: "test-agent".to_string(), task: "implement feature".to_string(), - static_model: Some("static-model".to_string()), + // Allow-list-conformant static model. + static_model: Some("kimi-for-coding/k2p5".to_string()), cli_tool: "opencode".to_string(), layer: crate::config::AgentLayer::Core, session_id: None, @@ -838,7 +961,7 @@ mod tests { let store = TelemetryStore::new(3600); store .record(CompletionEvent { - model: "limited-model".to_string(), + model: "opencode-go/limited-model".to_string(), session_id: "test".to_string(), completed_at: chrono::Utc::now(), latency_ms: 0, @@ -856,7 +979,8 @@ mod tests { Some(Arc::new(store)), ); - let ctx = create_test_context_with_static_model("agent", "task", "limited-model"); + let ctx = + create_test_context_with_static_model("agent", "task", "opencode-go/limited-model"); let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; assert!( @@ -878,7 +1002,7 @@ mod tests { for _ in 0..10 { store .record(CompletionEvent { - model: "fast-model".to_string(), + model: "opencode-go/fast-model".to_string(), session_id: "test".to_string(), completed_at: chrono::Utc::now(), latency_ms: 200, @@ -902,7 +1026,11 @@ mod tests { Some(Arc::new(store)), ); - let ctx = create_test_context_with_static_model("agent", "implement feature", "fast-model"); + let ctx = create_test_context_with_static_model( + "agent", + "implement feature", + "opencode-go/fast-model", + ); let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; assert!( @@ -914,4 +1042,160 @@ mod tests { "rationale should mention telemetry" ); } + + // --- C1/C3 allow-list filter --- + + #[tokio::test] + async fn test_c3_banned_static_model_falls_back_to_cli_default() { + // A malformed static model that names a banned provider must not + // survive routing; the engine falls back to the CLI default. + let engine = test_engine(); + let ctx = create_test_context_with_static_model( + "agent-with-bad-static", + "task", + "github-copilot/gpt-4.1", + ); + let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; + + assert_eq!(decision.candidate.source, RouteSource::CliDefault); + assert!(decision.candidate.model.is_empty()); + assert!( + decision.rationale.contains("C1/C3 allow-list"), + "rationale should call out the filter: {}", + decision.rationale + ); + } + + #[tokio::test] + async fn test_c3_banned_minimax_prefix_rejected_but_plan_allowed() { + let engine = test_engine(); + + let banned_ctx = + create_test_context_with_static_model("agent", "task", "minimax/MiniMax-M2.5"); + let banned_decision = engine + .decide_route(&banned_ctx, &BudgetVerdict::Uncapped) + .await; + assert_eq!(banned_decision.candidate.source, RouteSource::CliDefault); + + let allowed_ctx = create_test_context_with_static_model( + "agent", + "task", + "minimax-coding-plan/MiniMax-M2.5", + ); + let allowed_decision = engine + .decide_route(&allowed_ctx, &BudgetVerdict::Uncapped) + .await; + assert_eq!(allowed_decision.candidate.source, RouteSource::StaticConfig); + assert_eq!( + allowed_decision.candidate.model, + "minimax-coding-plan/MiniMax-M2.5" + ); + } + + #[tokio::test] + async fn test_c1_allowed_subscription_prefix_passes() { + let engine = test_engine(); + let ctx = create_test_context_with_static_model("agent", "task", "kimi-for-coding/k2p5"); + let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; + assert_eq!(decision.candidate.source, RouteSource::StaticConfig); + assert_eq!(decision.candidate.model, "kimi-for-coding/k2p5"); + } + + // --- Provider-budget gate --- + + #[tokio::test] + async fn test_provider_budget_exhausted_drops_candidate() { + // opencode-go capped at $0.50/hour; push past it and the static + // candidate must be stripped before scoring, forcing CLI default. + let tracker = + ProviderBudgetTracker::new(vec![crate::provider_budget::ProviderBudgetConfig { + id: "opencode-go".to_string(), + max_hour_cents: Some(50), + max_day_cents: None, + error_signatures: None, + }]); + let _ = tracker.record_cost("opencode-go", 1.00); + let engine = RoutingDecisionEngine::with_provider_budget( + None, + Vec::new(), + terraphim_router::Router::new(), + None, + Some(Arc::new(tracker)), + ); + let ctx = + create_test_context_with_static_model("agent", "task", "opencode-go/minimax-m2.5"); + let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; + assert_eq!(decision.candidate.source, RouteSource::CliDefault); + assert!( + decision.rationale.contains("provider-budget"), + "rationale should call out provider-budget: {}", + decision.rationale + ); + } + + #[tokio::test] + async fn test_provider_budget_near_exhaustion_deprioritises() { + // opencode-go at ~85% (well into the NearExhaustion band) and + // kimi-for-coding has no cap. Both candidates survive the filter + // but opencode-go's score is knocked down so the healthier + // kimi-for-coding wins. Rationale must cite the near-exhaustion. + let tracker = + ProviderBudgetTracker::new(vec![crate::provider_budget::ProviderBudgetConfig { + id: "opencode-go".to_string(), + max_hour_cents: Some(100), + max_day_cents: None, + error_signatures: None, + }]); + // Spend $0.85 -> 85% of $1/hr cap -> NearExhaustion. + let _ = tracker.record_cost("opencode-go", 0.85); + let engine = RoutingDecisionEngine::with_provider_budget( + None, + Vec::new(), + terraphim_router::Router::new(), + None, + Some(Arc::new(tracker)), + ); + // Static model references opencode-go; confidence 0.8. + let ctx = + create_test_context_with_static_model("agent", "task", "opencode-go/minimax-m2.5"); + let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; + + // Near-exhaustion candidate still wins because it's the only + // signal, but the score must be penalised and the rationale + // must mention the provider key. + assert_eq!(decision.candidate.source, RouteSource::StaticConfig); + assert!( + decision.budget_influenced, + "budget_influenced flag should be set" + ); + assert!( + decision.rationale.contains("opencode-go"), + "rationale should name the near-exhausted provider: {}", + decision.rationale + ); + } + + #[tokio::test] + async fn test_provider_budget_uncapped_provider_unaffected() { + let tracker = + ProviderBudgetTracker::new(vec![crate::provider_budget::ProviderBudgetConfig { + id: "opencode-go".to_string(), + max_hour_cents: Some(100), + max_day_cents: None, + error_signatures: None, + }]); + // kimi-for-coding has no config entry -> Uncapped -> no effect. + let engine = RoutingDecisionEngine::with_provider_budget( + None, + Vec::new(), + terraphim_router::Router::new(), + None, + Some(Arc::new(tracker)), + ); + let ctx = create_test_context_with_static_model("agent", "task", "kimi-for-coding/k2p5"); + let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; + assert_eq!(decision.candidate.source, RouteSource::StaticConfig); + assert_eq!(decision.candidate.model, "kimi-for-coding/k2p5"); + assert!(!decision.budget_influenced); + } } diff --git a/crates/terraphim_orchestrator/src/dispatcher.rs b/crates/terraphim_orchestrator/src/dispatcher.rs index b923631db..b61d22636 100644 --- a/crates/terraphim_orchestrator/src/dispatcher.rs +++ b/crates/terraphim_orchestrator/src/dispatcher.rs @@ -1,8 +1,13 @@ //! Unified dispatch queue for both time-driven and issue-driven modes. //! -//! Provides a priority queue with fairness between different dispatch sources. +//! Provides a priority queue with fairness between different dispatch sources, +//! including per-project round-robin within the same priority score so that +//! one noisy project cannot starve the others. -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; + +/// Synthetic project id used for legacy single-project mode. +pub const LEGACY_PROJECT_ID: &str = "__global__"; /// A dispatch task from any source. #[derive(Debug, Clone)] @@ -15,6 +20,8 @@ pub enum DispatchTask { task: String, /// Layer (Safety, Core, Growth). layer: crate::AgentLayer, + /// Project id this agent belongs to. + project: String, }, /// Issue-driven agent dispatch. IssueDriven { @@ -26,6 +33,8 @@ pub enum DispatchTask { priority: Option, /// PageRank score (higher = more important). pagerank_score: Option, + /// Project id that owns the tracker producing this issue. + project: String, }, /// Mention-driven agent dispatch (from @adf: comment mentions). MentionDriven { @@ -37,9 +46,22 @@ pub enum DispatchTask { comment_id: u64, /// Full comment body for context. context: String, + /// Project id this mention was detected in. + project: String, }, } +impl DispatchTask { + /// Project id this task is associated with. + pub fn project(&self) -> &str { + match self { + DispatchTask::TimeDriven { project, .. } => project, + DispatchTask::IssueDriven { project, .. } => project, + DispatchTask::MentionDriven { project, .. } => project, + } + } +} + /// Priority wrapper for dispatch tasks. #[derive(Debug)] struct PrioritizedTask { @@ -53,10 +75,16 @@ struct PrioritizedTask { /// Unified dispatcher queue. pub struct Dispatcher { - /// Internal priority queue. + /// Internal priority queue (sorted ascending by score, then seq). queue: VecDeque, /// Sequence counter for FIFO ordering within same priority. seq_counter: u64, + /// Monotonic dequeue counter used to timestamp per-project service order. + dequeue_counter: u64, + /// Last dequeue counter value recorded for each project. Projects that + /// have never been dequeued are treated as "least recently served" and + /// are preferred for round-robin fairness. + last_dequeue_seq: HashMap, /// Statistics. stats: DispatcherStats, } @@ -70,8 +98,10 @@ pub struct DispatcherStats { pub total_dequeued: u64, /// Current queue depth. pub current_depth: usize, - /// Tasks by source. - pub by_source: std::collections::HashMap, + /// Current in-queue task counts by source. + pub by_source: HashMap, + /// Current in-queue task counts by project. + pub by_project: HashMap, } impl Dispatcher { @@ -80,6 +110,8 @@ impl Dispatcher { Self { queue: VecDeque::new(), seq_counter: 0, + dequeue_counter: 0, + last_dequeue_seq: HashMap::new(), stats: DispatcherStats::default(), } } @@ -91,50 +123,75 @@ impl Dispatcher { let score = self.compute_priority(&task); let source = self.task_source(&task); + let project = task.project().to_string(); let pt = PrioritizedTask { seq, score, task }; - // Insert maintaining priority order (lower score = higher priority) + // Maintain insertion order by (score, seq) for deterministic FIFO + // within equal scores. Round-robin fairness is applied in dequeue(). let insert_pos = self .queue .iter() .position(|existing| { - // Higher priority (lower score) comes first - // For same score, earlier sequence comes first (FIFO) existing.score > score || (existing.score == score && existing.seq > seq) }) .unwrap_or(self.queue.len()); self.queue.insert(insert_pos, pt); - // Update stats self.stats.total_enqueued += 1; self.stats.current_depth = self.queue.len(); *self.stats.by_source.entry(source).or_insert(0) += 1; + *self.stats.by_project.entry(project).or_insert(0) += 1; tracing::debug!(score, depth = self.stats.current_depth, "task enqueued"); } - /// Dequeue the highest priority task. + /// Dequeue the highest priority task, applying per-project round-robin + /// among tasks that share the lowest priority score. pub fn dequeue(&mut self) -> Option { - let task = self.queue.pop_front().map(|pt| { - let source = self.task_source(&pt.task); - - // Update stats - self.stats.total_dequeued += 1; - self.stats.current_depth = self.queue.len(); - if let Some(count) = self.stats.by_source.get_mut(&source) { - *count -= 1; + if self.queue.is_empty() { + return None; + } + + let min_score = self.queue.front().map(|pt| pt.score)?; + + // Find the tied-score prefix and select the entry whose project was + // least recently dequeued. Break ties by seq (FIFO). + let mut best_idx: usize = 0; + let mut best_key: Option<(u64, u64)> = None; + for (i, pt) in self.queue.iter().enumerate() { + if pt.score != min_score { + break; + } + let project = pt.task.project(); + let last = self.last_dequeue_seq.get(project).copied().unwrap_or(0); + let key = (last, pt.seq); + if best_key.map_or(true, |b| key < b) { + best_key = Some(key); + best_idx = i; } + } - pt.task - }); + let pt = self.queue.remove(best_idx)?; + let source = self.task_source(&pt.task); + let project = pt.task.project().to_string(); - if task.is_some() { - tracing::debug!(depth = self.stats.current_depth, "task dequeued"); + self.dequeue_counter += 1; + self.last_dequeue_seq + .insert(project.clone(), self.dequeue_counter); + + self.stats.total_dequeued += 1; + self.stats.current_depth = self.queue.len(); + if let Some(count) = self.stats.by_source.get_mut(&source) { + *count = count.saturating_sub(1); + } + if let Some(count) = self.stats.by_project.get_mut(&project) { + *count = count.saturating_sub(1); } - task + tracing::debug!(depth = self.stats.current_depth, "task dequeued"); + Some(pt.task) } /// Peek at the next task without removing it. @@ -155,35 +212,21 @@ impl Dispatcher { /// Compute priority score for a task (lower = higher priority). fn compute_priority(&self, task: &DispatchTask) -> i64 { match task { - DispatchTask::TimeDriven { layer, .. } => { - // Safety agents have highest priority (lowest score) - // Core = medium, Growth = lowest - match layer { - crate::AgentLayer::Safety => 0, - crate::AgentLayer::Core => 1000, - crate::AgentLayer::Growth => 2000, - } - } + DispatchTask::TimeDriven { layer, .. } => match layer { + crate::AgentLayer::Safety => 0, + crate::AgentLayer::Core => 1000, + crate::AgentLayer::Growth => 2000, + }, DispatchTask::IssueDriven { priority, pagerank_score, .. } => { - // Base priority from issue priority (lower = more urgent) let base = priority.map(|p| p as i64 * 100).unwrap_or(500); - - // Adjust by PageRank (higher PageRank = more important = lower score) let pagerank_bonus = pagerank_score.map(|pr| -(pr * 100.0) as i64).unwrap_or(0); - - // Time-driven gets slight priority over issue-driven at same urgency base + pagerank_bonus + 3000 } - DispatchTask::MentionDriven { .. } => { - // Mention-driven tasks sit between Safety (0) and Core (1000). - // Priority 200 ensures mentions are handled promptly but Safety - // agent restarts remain the highest priority. - 200 - } + DispatchTask::MentionDriven { .. } => 200, } } @@ -207,15 +250,24 @@ impl Default for Dispatcher { mod tests { use super::*; + fn time_task(name: &str, project: &str, layer: crate::AgentLayer) -> DispatchTask { + DispatchTask::TimeDriven { + name: name.into(), + task: "task".into(), + layer, + project: project.into(), + } + } + #[test] fn test_enqueue_dequeue() { let mut dispatcher = Dispatcher::new(); - dispatcher.enqueue(DispatchTask::TimeDriven { - name: "test".into(), - task: "do something".into(), - layer: crate::AgentLayer::Safety, - }); + dispatcher.enqueue(time_task( + "test", + LEGACY_PROJECT_ID, + crate::AgentLayer::Safety, + )); assert_eq!(dispatcher.depth(), 1); @@ -228,26 +280,22 @@ mod tests { fn test_priority_ordering() { let mut dispatcher = Dispatcher::new(); - // Enqueue in reverse priority order - dispatcher.enqueue(DispatchTask::TimeDriven { - name: "growth".into(), - task: "task".into(), - layer: crate::AgentLayer::Growth, - }); + dispatcher.enqueue(time_task( + "growth", + LEGACY_PROJECT_ID, + crate::AgentLayer::Growth, + )); + dispatcher.enqueue(time_task( + "core", + LEGACY_PROJECT_ID, + crate::AgentLayer::Core, + )); + dispatcher.enqueue(time_task( + "safety", + LEGACY_PROJECT_ID, + crate::AgentLayer::Safety, + )); - dispatcher.enqueue(DispatchTask::TimeDriven { - name: "core".into(), - task: "task".into(), - layer: crate::AgentLayer::Core, - }); - - dispatcher.enqueue(DispatchTask::TimeDriven { - name: "safety".into(), - task: "task".into(), - layer: crate::AgentLayer::Safety, - }); - - // Should dequeue in priority order: Safety, Core, Growth if let Some(DispatchTask::TimeDriven { name, .. }) = dispatcher.dequeue() { assert_eq!(name, "safety"); } @@ -260,20 +308,11 @@ mod tests { } #[test] - fn test_fifo_within_same_priority() { + fn test_fifo_within_same_priority_and_project() { let mut dispatcher = Dispatcher::new(); - dispatcher.enqueue(DispatchTask::TimeDriven { - name: "first".into(), - task: "task".into(), - layer: crate::AgentLayer::Safety, - }); - - dispatcher.enqueue(DispatchTask::TimeDriven { - name: "second".into(), - task: "task".into(), - layer: crate::AgentLayer::Safety, - }); + dispatcher.enqueue(time_task("first", "alpha", crate::AgentLayer::Safety)); + dispatcher.enqueue(time_task("second", "alpha", crate::AgentLayer::Safety)); if let Some(DispatchTask::TimeDriven { name, .. }) = dispatcher.dequeue() { assert_eq!(name, "first"); @@ -292,6 +331,7 @@ mod tests { title: "Low PageRank".into(), priority: Some(1), pagerank_score: Some(0.15), + project: "alpha".into(), }); dispatcher.enqueue(DispatchTask::IssueDriven { @@ -299,9 +339,9 @@ mod tests { title: "High PageRank".into(), priority: Some(1), pagerank_score: Some(2.5), + project: "alpha".into(), }); - // Higher PageRank should come first if let Some(DispatchTask::IssueDriven { identifier, .. }) = dispatcher.dequeue() { assert_eq!(identifier, "high-pr"); } @@ -314,17 +354,13 @@ mod tests { fn test_stats_tracking() { let mut dispatcher = Dispatcher::new(); - dispatcher.enqueue(DispatchTask::TimeDriven { - name: "safety".into(), - task: "task".into(), - layer: crate::AgentLayer::Safety, - }); - + dispatcher.enqueue(time_task("safety", "alpha", crate::AgentLayer::Safety)); dispatcher.enqueue(DispatchTask::IssueDriven { identifier: "issue-1".into(), title: "Issue".into(), priority: Some(1), pagerank_score: None, + project: "beta".into(), }); let stats = dispatcher.stats(); @@ -332,6 +368,8 @@ mod tests { assert_eq!(stats.current_depth, 2); assert_eq!(stats.by_source.get("time_driven"), Some(&1)); assert_eq!(stats.by_source.get("issue_driven"), Some(&1)); + assert_eq!(stats.by_project.get("alpha"), Some(&1)); + assert_eq!(stats.by_project.get("beta"), Some(&1)); dispatcher.dequeue(); @@ -339,4 +377,58 @@ mod tests { assert_eq!(stats.total_dequeued, 1); assert_eq!(stats.current_depth, 1); } + + #[test] + fn test_round_robin_across_projects_within_same_score() { + let mut dispatcher = Dispatcher::new(); + + // Three tasks for alpha enqueued before beta's single task. + dispatcher.enqueue(time_task("a1", "alpha", crate::AgentLayer::Core)); + dispatcher.enqueue(time_task("a2", "alpha", crate::AgentLayer::Core)); + dispatcher.enqueue(time_task("a3", "alpha", crate::AgentLayer::Core)); + dispatcher.enqueue(time_task("b1", "beta", crate::AgentLayer::Core)); + + // First dequeue: alpha is earliest by seq, and neither project has + // been served -> alpha wins on seq tie-break. + let t1 = dispatcher.dequeue().unwrap(); + assert_eq!(t1.project(), "alpha"); + + // Second dequeue: beta has never been served, alpha now has a + // recent dequeue seq -> beta should jump ahead of alpha's backlog. + let t2 = dispatcher.dequeue().unwrap(); + assert_eq!(t2.project(), "beta"); + + // Remaining two tasks are alpha only; FIFO among them. + let t3 = dispatcher.dequeue().unwrap(); + assert_eq!(t3.project(), "alpha"); + let t4 = dispatcher.dequeue().unwrap(); + assert_eq!(t4.project(), "alpha"); + + assert!(dispatcher.dequeue().is_none()); + } + + #[test] + fn test_round_robin_does_not_override_priority() { + let mut dispatcher = Dispatcher::new(); + + // High-priority alpha task should dequeue before low-priority beta + // even though beta has never been served. + dispatcher.enqueue(time_task("a-growth", "alpha", crate::AgentLayer::Growth)); + dispatcher.enqueue(time_task("a-safety", "alpha", crate::AgentLayer::Safety)); + dispatcher.enqueue(time_task("b-growth", "beta", crate::AgentLayer::Growth)); + + let t1 = dispatcher.dequeue().unwrap(); + match t1 { + DispatchTask::TimeDriven { name, .. } => assert_eq!(name, "a-safety"), + _ => panic!("expected TimeDriven"), + } + + // Now alpha has been served; beta's Growth should win round-robin + // over alpha's Growth backlog. + let t2 = dispatcher.dequeue().unwrap(); + assert_eq!(t2.project(), "beta"); + + let t3 = dispatcher.dequeue().unwrap(); + assert_eq!(t3.project(), "alpha"); + } } diff --git a/crates/terraphim_orchestrator/src/dual_mode.rs b/crates/terraphim_orchestrator/src/dual_mode.rs index ce9d2851f..463a0af5f 100644 --- a/crates/terraphim_orchestrator/src/dual_mode.rs +++ b/crates/terraphim_orchestrator/src/dual_mode.rs @@ -3,6 +3,7 @@ //! Manages both time-driven and issue-driven agent execution modes //! with shared concurrency control and unified status. +use crate::concurrency::ProjectCaps; use crate::{ AgentDefinition, AgentOrchestrator, CompoundReviewResult, ConcurrencyController, DispatcherStats, FairnessPolicy, HandoffContext, ModeQuotas, OrchestratorConfig, ScheduleEvent, @@ -101,10 +102,17 @@ struct TimeModeComponents { shutdown_rx: watch::Receiver, } -/// Components for issue mode. -struct IssueModeComponents { +/// Per-project tracker state for issue mode. +struct ProjectTracker { tracker: Box, workflow: WorkflowConfig, +} + +/// Components for issue mode. +struct IssueModeComponents { + /// Trackers keyed by project id. Legacy single-project mode uses + /// [`crate::dispatcher::LEGACY_PROJECT_ID`] as the key. + running_trackers: HashMap, shutdown_rx: watch::Receiver, } @@ -113,9 +121,26 @@ impl DualModeOrchestrator { pub fn new(config: OrchestratorConfig) -> Result { let base = AgentOrchestrator::new(config.clone())?; + // Collect per-project concurrency caps from project definitions. + let project_caps: HashMap = config + .projects + .iter() + .filter_map(|p| { + p.max_concurrent_agents.map(|max| { + ( + p.id.clone(), + ProjectCaps { + max_concurrent_agents: max, + max_concurrent_mention_agents: p.max_concurrent_mention_agents, + }, + ) + }) + }) + .collect(); + // Create concurrency controller let concurrency = if let Some(ref workflow) = config.workflow { - ConcurrencyController::new( + ConcurrencyController::with_project_caps( workflow.concurrency.global_max, ModeQuotas { time_max: workflow @@ -129,9 +154,15 @@ impl DualModeOrchestrator { .fairness .parse() .unwrap_or(FairnessPolicy::RoundRobin), + project_caps, ) } else { - ConcurrencyController::new(10, ModeQuotas::default(), FairnessPolicy::RoundRobin) + ConcurrencyController::with_project_caps( + 10, + ModeQuotas::default(), + FairnessPolicy::RoundRobin, + project_caps, + ) }; // Create shared state @@ -156,28 +187,61 @@ impl DualModeOrchestrator { }) }; - // Setup issue mode if configured - let issue_mode = if let Some(ref workflow) = config.workflow { - if workflow.enabled { + // Setup issue mode if configured. Build one tracker per project when + // multi-project config is present; otherwise fall back to the top-level + // workflow with the legacy global project id. + let mut running_trackers: HashMap = HashMap::new(); + + if !config.projects.is_empty() { + for project in &config.projects { + let Some(workflow) = project.workflow.as_ref() else { + continue; + }; + if !workflow.enabled { + continue; + } match create_tracker(workflow) { Ok(tracker) => { - let shutdown_rx = state.shutdown_tx.subscribe(); - Some(IssueModeComponents { - tracker, - workflow: workflow.clone(), - shutdown_rx, - }) + running_trackers.insert( + project.id.clone(), + ProjectTracker { + tracker, + workflow: workflow.clone(), + }, + ); } - Err(e) => { - warn!("failed to create issue tracker: {}", e); - None + Err(e) => warn!( + project = %project.id, + "failed to create per-project issue tracker: {}", + e + ), + } + } + } else if let Some(ref workflow) = config.workflow { + if workflow.enabled { + match create_tracker(workflow) { + Ok(tracker) => { + running_trackers.insert( + crate::dispatcher::LEGACY_PROJECT_ID.to_string(), + ProjectTracker { + tracker, + workflow: workflow.clone(), + }, + ); } + Err(e) => warn!("failed to create issue tracker: {}", e), } - } else { - None } - } else { + } + + let issue_mode = if running_trackers.is_empty() { None + } else { + let shutdown_rx = state.shutdown_tx.subscribe(); + Some(IssueModeComponents { + running_trackers, + shutdown_rx, + }) }; Ok(Self { @@ -420,8 +484,12 @@ async fn run_time_mode(components: TimeModeComponents, state: SharedState) { event = scheduler.next_event() => { match event { ScheduleEvent::Spawn(agent) => { - // Try to acquire time-driven slot - match state.concurrency.acquire_time_driven().await { + let project = agent + .project + .clone() + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()); + // Try to acquire time-driven slot for this project + match state.concurrency.acquire_time_driven(&project).await { Some(permit) => { info!(agent_name = %agent.name, "spawning time-driven agent"); // Spawn agent here @@ -453,51 +521,78 @@ async fn run_time_mode(components: TimeModeComponents, state: SharedState) { } } -/// Run issue mode in background. +/// Run issue mode in background. Polls every configured tracker and +/// dispatches against its owning project id. async fn run_issue_mode(components: IssueModeComponents, state: SharedState) { - info!("starting issue mode task"); + info!( + projects = components.running_trackers.len(), + "starting issue mode task" + ); let IssueModeComponents { - tracker, - workflow, + running_trackers, mut shutdown_rx, } = components; - let poll_interval = Duration::from_secs(workflow.poll_interval_secs); + // Use the shortest configured poll interval across projects so every + // tracker gets polled at least as often as its own setting requires. A + // per-project cadence could be added later by moving each tracker into + // its own task; a single shared interval keeps the loop simple for now. + let poll_interval = running_trackers + .values() + .map(|p| p.workflow.poll_interval_secs) + .min() + .map(Duration::from_secs) + .unwrap_or_else(|| Duration::from_secs(60)); loop { tokio::select! { _ = tokio::time::sleep(poll_interval) => { - match tracker.fetch_candidate_issues().await { - Ok(issues) => { - info!(count = issues.len(), "fetched candidate issues"); - - for issue in issues { - // Skip blocked issues - if !issue.all_blockers_terminal(&workflow.tracker.states.terminal) { - continue; - } - - // Try to acquire issue-driven slot - match state.concurrency.acquire_issue_driven().await { - Some(permit) => { - info!( - issue_id = %issue.id, - title = %issue.title, - "dispatching issue-driven agent" - ); - // Spawn agent here - drop(permit); + for (project_id, project_tracker) in running_trackers.iter() { + let ProjectTracker { tracker, workflow } = project_tracker; + match tracker.fetch_candidate_issues().await { + Ok(issues) => { + info!( + project = %project_id, + count = issues.len(), + "fetched candidate issues" + ); + + for issue in issues { + // Skip blocked issues + if !issue.all_blockers_terminal(&workflow.tracker.states.terminal) { + continue; } - None => { - warn!("no slot available for issue-driven agent"); - break; // Stop trying until slots free up + + // Try to acquire issue-driven slot for this project. + match state + .concurrency + .acquire_issue_driven(project_id) + .await + { + Some(permit) => { + info!( + project = %project_id, + issue_id = %issue.id, + title = %issue.title, + "dispatching issue-driven agent" + ); + // Spawn agent here + drop(permit); + } + None => { + warn!( + project = %project_id, + "no slot available for issue-driven agent" + ); + break; // Stop trying for this project until slots free up + } } } } - } - Err(e) => { - error!("failed to fetch issues: {}", e); + Err(e) => { + error!(project = %project_id, "failed to fetch issues: {}", e); + } } } } diff --git a/crates/terraphim_orchestrator/src/error.rs b/crates/terraphim_orchestrator/src/error.rs index eb3feb82c..fe56b3340 100644 --- a/crates/terraphim_orchestrator/src/error.rs +++ b/crates/terraphim_orchestrator/src/error.rs @@ -55,4 +55,36 @@ pub enum OrchestratorError { #[error("flow template error: {0}")] FlowTemplateError(String), + + #[error( + "duplicate project id '{0}' (project ids must be unique across base + included configs)" + )] + DuplicateProjectId(String), + + #[error( + "agent '{agent}' references unknown project '{project}' (must match a Project.id in projects list)" + )] + UnknownAgentProject { agent: String, project: String }, + + #[error( + "flow '{flow}' references unknown project '{project}' (must match a Project.id in projects list)" + )] + UnknownFlowProject { flow: String, project: String }, + + #[error( + "banned LLM provider '{provider}' in {field} for agent '{agent}' (allowed: claude-code, opencode-go, kimi-for-coding, minimax-coding-plan, zai-coding-plan)" + )] + BannedProvider { + agent: String, + provider: String, + field: String, + }, + + #[error( + "mixed project mode: projects are defined but {kind} '{name}' has no project set; every agent and flow must declare a project" + )] + MixedProjectMode { kind: &'static str, name: String }, + + #[error("include glob '{pattern}' is invalid: {reason}")] + InvalidIncludeGlob { pattern: String, reason: String }, } diff --git a/crates/terraphim_orchestrator/src/error_signatures.rs b/crates/terraphim_orchestrator/src/error_signatures.rs new file mode 100644 index 000000000..557bbcaf7 --- /dev/null +++ b/crates/terraphim_orchestrator/src/error_signatures.rs @@ -0,0 +1,326 @@ +//! Per-provider stderr classification into throttle / flake / unknown. +//! +//! Complements [`crate::agent_run_record::ExitClassifier`]: the classifier +//! here consumes *configurable* per-provider regex lists so operators can +//! tune detection per CLI (`claude-code`, `opencode-go`, `kimi-for-coding`, +//! ...) without code changes. Matches feed the existing +//! [`terraphim_spawner::health::CircuitBreaker`] (via +//! [`crate::provider_probe::ProviderHealthMap`]) and the +//! [`crate::provider_budget::ProviderBudgetTracker`] -- nothing new is +//! invented here. +//! +//! Classification outcomes: +//! * **Throttle** -- provider quota / rate-limit hit. Trip the breaker and +//! force the hour+day budget windows past their caps so the routing +//! filter drops the provider until the next window rolls. +//! * **Flake** -- transient failure (timeout, EOF, connection reset). Do +//! NOT trip the breaker; the dispatch layer retries with the next entry +//! in the pool. +//! * **Unknown** -- neither list matched. Escalate (fleet-meta issue) so a +//! human can classify the pattern. Unknown is also counted as a soft +//! failure so a pathological provider that repeatedly emits unclassified +//! errors still eventually opens the breaker. + +use std::collections::HashMap; +use std::fmt; + +use regex::{Regex, RegexBuilder}; +use serde::{Deserialize, Serialize}; + +/// Classifier verdict returned by [`classify`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { + /// Provider has hit its quota / rate limit -- back off for a window. + Throttle, + /// Transient failure (timeout, EOF). Retry next pool entry. + Flake, + /// No pattern matched. Escalate for human review. + Unknown, +} + +impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ErrorKind::Throttle => f.write_str("throttle"), + ErrorKind::Flake => f.write_str("flake"), + ErrorKind::Unknown => f.write_str("unknown"), + } + } +} + +/// Serialised form of per-provider error signatures (matches the TOML +/// layout under `[[providers]].error_signatures`). +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProviderErrorSignatures { + /// Regex patterns matching rate-limit / quota errors. + #[serde(default)] + pub throttle: Vec, + /// Regex patterns matching transient errors (timeout, EOF, reset). + #[serde(default)] + pub flake: Vec, +} + +/// Compile error building per-provider regex patterns. +#[derive(Debug)] +pub struct CompileError { + pub provider: String, + pub pattern: String, + pub source: regex::Error, +} + +impl fmt::Display for CompileError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "invalid error-signature regex for provider '{}': `{}`: {}", + self.provider, self.pattern, self.source + ) + } +} + +impl std::error::Error for CompileError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } +} + +/// Runtime-compiled signatures for a single provider. +#[derive(Debug, Clone, Default)] +pub struct CompiledSignatures { + /// Regexes that classify a stderr line as [`ErrorKind::Throttle`]. + pub throttle: Vec, + /// Regexes that classify a stderr line as [`ErrorKind::Flake`]. + pub flake: Vec, +} + +impl CompiledSignatures { + /// Compile a [`ProviderErrorSignatures`] into runtime regexes. + /// + /// Patterns are compiled case-insensitively so the config can use the + /// canonical spelling (`429`, `rate limit`, `timeout`) and still match + /// mixed-case CLI output. + pub fn compile(provider: &str, sigs: &ProviderErrorSignatures) -> Result { + let throttle = compile_list(provider, &sigs.throttle)?; + let flake = compile_list(provider, &sigs.flake)?; + Ok(Self { throttle, flake }) + } + + /// Whether this provider has any signatures configured at all. + pub fn is_empty(&self) -> bool { + self.throttle.is_empty() && self.flake.is_empty() + } +} + +fn compile_list(provider: &str, patterns: &[String]) -> Result, CompileError> { + patterns + .iter() + .map(|p| { + RegexBuilder::new(p) + .case_insensitive(true) + .build() + .map_err(|e| CompileError { + provider: provider.to_string(), + pattern: p.clone(), + source: e, + }) + }) + .collect() +} + +/// Map of compiled signatures keyed by provider id. +/// +/// Missing providers (or providers with empty signature lists) are treated +/// as "no signatures configured" and their stderr classifies as +/// [`ErrorKind::Unknown`] -- fail-safe default. +pub type ProviderSignatureMap = HashMap; + +/// Build a signature map from the raw config list. Invalid regexes surface +/// as `CompileError` so misconfiguration fails loud at startup rather than +/// silently disabling classification at runtime. +pub fn build_signature_map( + configs: &[crate::provider_budget::ProviderBudgetConfig], +) -> Result { + let mut map = HashMap::new(); + for cfg in configs { + if let Some(sigs) = cfg.error_signatures.as_ref() { + let compiled = CompiledSignatures::compile(&cfg.id, sigs)?; + if !compiled.is_empty() { + map.insert(cfg.id.clone(), compiled); + } + } + } + Ok(map) +} + +/// Classify a stderr snippet against the provider's compiled signatures. +/// +/// Throttle is checked first so a message matching both lists (e.g. +/// "timeout waiting for rate limit reset") is treated as a throttle. +/// Returns [`ErrorKind::Unknown`] when no pattern matches or when the +/// provider has no signatures configured (`None`). +pub fn classify(stderr: &str, signatures: Option<&CompiledSignatures>) -> ErrorKind { + let Some(sigs) = signatures else { + return ErrorKind::Unknown; + }; + if sigs.throttle.iter().any(|re| re.is_match(stderr)) { + return ErrorKind::Throttle; + } + if sigs.flake.iter().any(|re| re.is_match(stderr)) { + return ErrorKind::Flake; + } + ErrorKind::Unknown +} + +/// Classify a list of stderr lines by joining them (newline-separated) +/// and running [`classify`]. Convenience wrapper for the spawn-exit path +/// where stderr is captured line-by-line. +pub fn classify_lines(lines: &[String], signatures: Option<&CompiledSignatures>) -> ErrorKind { + if lines.is_empty() { + return ErrorKind::Unknown; + } + let joined = lines.join("\n"); + classify(&joined, signatures) +} + +/// Build a stable dedupe key for an unknown-error escalation: provider +/// plus the leading 20 chars of the stderr (both-ends trimmed + lowercased). +/// The short prefix + lowercase normalisation means minor suffix variance +/// (trailing newlines, mixed case, extra detail) dedupes to one key so we +/// don't spam fleet-meta with duplicate issues for the same stderr shape. +pub fn unknown_dedupe_key(provider: &str, stderr: &str) -> String { + let head: String = stderr + .trim() + .chars() + .take(20) + .collect::() + .to_lowercase(); + format!("{}::{}", provider, head) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::provider_budget::ProviderBudgetConfig; + + fn sigs(throttle: &[&str], flake: &[&str]) -> ProviderErrorSignatures { + ProviderErrorSignatures { + throttle: throttle.iter().map(|s| s.to_string()).collect(), + flake: flake.iter().map(|s| s.to_string()).collect(), + } + } + + #[test] + fn unknown_when_no_signatures() { + assert_eq!(classify("anything", None), ErrorKind::Unknown); + } + + #[test] + fn throttle_matches_case_insensitive() { + let compiled = + CompiledSignatures::compile("zai-coding-plan", &sigs(&["insufficient.?balance"], &[])) + .unwrap(); + assert_eq!( + classify("ERROR: Insufficient Balance on account", Some(&compiled)), + ErrorKind::Throttle + ); + } + + #[test] + fn flake_matches_timeout() { + let compiled = + CompiledSignatures::compile("claude-code", &sigs(&[], &["timeout", "EOF"])).unwrap(); + assert_eq!( + classify("stream timeout after 60s", Some(&compiled)), + ErrorKind::Flake + ); + } + + #[test] + fn throttle_beats_flake_when_both_match() { + let compiled = CompiledSignatures::compile( + "claude-code", + &sigs(&["rate.?limit"], &["rate.?limit.*timeout"]), + ) + .unwrap(); + // Both lists match, throttle wins. + assert_eq!( + classify("rate limit timeout", Some(&compiled)), + ErrorKind::Throttle + ); + } + + #[test] + fn unknown_when_no_pattern_matches() { + let compiled = + CompiledSignatures::compile("claude-code", &sigs(&["429"], &["timeout"])).unwrap(); + assert_eq!( + classify("panic: internal assertion failed", Some(&compiled)), + ErrorKind::Unknown + ); + } + + #[test] + fn classify_lines_joins_and_matches() { + let compiled = + CompiledSignatures::compile("kimi-for-coding", &sigs(&["quota"], &["EOF"])).unwrap(); + let lines = vec![ + "starting run".to_string(), + "error: daily quota exceeded".to_string(), + ]; + assert_eq!(classify_lines(&lines, Some(&compiled)), ErrorKind::Throttle); + } + + #[test] + fn classify_lines_empty_is_unknown() { + let compiled = + CompiledSignatures::compile("kimi-for-coding", &sigs(&["quota"], &[])).unwrap(); + assert_eq!(classify_lines(&[], Some(&compiled)), ErrorKind::Unknown); + } + + #[test] + fn compile_error_wraps_regex_error() { + let err = CompiledSignatures::compile("bad", &sigs(&["[unterminated"], &[])).unwrap_err(); + assert_eq!(err.provider, "bad"); + assert_eq!(err.pattern, "[unterminated"); + } + + #[test] + fn build_signature_map_skips_providers_without_signatures() { + let configs = vec![ + ProviderBudgetConfig { + id: "opencode-go".to_string(), + error_signatures: Some(sigs(&["429"], &["timeout"])), + ..Default::default() + }, + ProviderBudgetConfig { + id: "no-sigs".to_string(), + error_signatures: None, + ..Default::default() + }, + ProviderBudgetConfig { + id: "empty-sigs".to_string(), + error_signatures: Some(ProviderErrorSignatures::default()), + ..Default::default() + }, + ]; + let map = build_signature_map(&configs).unwrap(); + assert!(map.contains_key("opencode-go")); + assert!(!map.contains_key("no-sigs")); + assert!(!map.contains_key("empty-sigs")); + } + + #[test] + fn dedupe_key_is_stable_and_lowercase() { + let k1 = unknown_dedupe_key("claude-code", " Unexpected JSON token\n"); + let k2 = unknown_dedupe_key("claude-code", "UNEXPECTED JSON token with extra suffix"); + assert_eq!(k1, k2); + assert!(k1.starts_with("claude-code::")); + } + + #[test] + fn display_matches_expected_tokens() { + assert_eq!(ErrorKind::Throttle.to_string(), "throttle"); + assert_eq!(ErrorKind::Flake.to_string(), "flake"); + assert_eq!(ErrorKind::Unknown.to_string(), "unknown"); + } +} diff --git a/crates/terraphim_orchestrator/src/flow/config.rs b/crates/terraphim_orchestrator/src/flow/config.rs index 0da7db9ed..35c5a7b3e 100644 --- a/crates/terraphim_orchestrator/src/flow/config.rs +++ b/crates/terraphim_orchestrator/src/flow/config.rs @@ -3,6 +3,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FlowDefinition { pub name: String, + /// Project this flow belongs to. Required -- flows are per-project only (D14). + /// Must match a `Project.id` when projects are defined. + pub project: String, #[serde(default)] pub schedule: Option, // cron expression pub repo_path: String, @@ -89,6 +92,7 @@ mod tests { fn test_flow_config_parse_minimal() { let toml_str = r#" name = "test-flow" +project = "default" repo_path = "/tmp/repo" [[steps]] @@ -99,6 +103,7 @@ command = "cargo build" let flow: FlowDefinition = toml::from_str(toml_str).unwrap(); assert_eq!(flow.name, "test-flow"); + assert_eq!(flow.project, "default"); assert_eq!(flow.repo_path, "/tmp/repo"); assert_eq!(flow.base_branch, "main"); // default assert!(flow.schedule.is_none()); @@ -116,6 +121,7 @@ command = "cargo build" fn test_flow_config_parse_full() { let toml_str = r#" name = "compound-review-v2" +project = "terraphim" schedule = "0 2 * * *" repo_path = "/home/user/project" base_branch = "develop" diff --git a/crates/terraphim_orchestrator/src/flow/executor.rs b/crates/terraphim_orchestrator/src/flow/executor.rs index 212cddac8..c3dfbace7 100644 --- a/crates/terraphim_orchestrator/src/flow/executor.rs +++ b/crates/terraphim_orchestrator/src/flow/executor.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::PathBuf; use chrono::Utc; @@ -9,13 +10,26 @@ use super::envelope::StepEnvelope; use super::state::{FlowRunState, FlowRunStatus}; use crate::config::{AgentDefinition, AgentLayer}; use crate::error::OrchestratorError; -use terraphim_spawner::{AgentSpawner, OutputEvent, SpawnRequest}; +use terraphim_spawner::{AgentSpawner, OutputEvent, SpawnContext, SpawnRequest}; use terraphim_types::capability::Provider; +/// Per-project runtime metadata used to build a [`SpawnContext`] for flow +/// steps. Populated from `OrchestratorConfig.projects` when FlowExecutor is +/// constructed. +#[derive(Debug, Clone, Default)] +pub struct ProjectRuntime { + pub working_dir: PathBuf, + pub gitea_owner: Option, + pub gitea_repo: Option, +} + pub struct FlowExecutor { pub working_dir: PathBuf, pub spawner: AgentSpawner, pub flow_state_dir: PathBuf, + /// Per-project runtime metadata, keyed by project id. Missing entries + /// mean "use the FlowExecutor's top-level working_dir" (legacy mode). + pub projects: HashMap, } impl FlowExecutor { @@ -24,7 +38,33 @@ impl FlowExecutor { working_dir: working_dir.clone(), spawner: AgentSpawner::new().with_working_dir(&working_dir), flow_state_dir, + projects: HashMap::new(), + } + } + + /// Builder: attach per-project runtime metadata. + pub fn with_projects(mut self, projects: HashMap) -> Self { + self.projects = projects; + self + } + + /// Build a [`SpawnContext`] for the given flow's project. If the project + /// id is unknown or legacy, returns [`SpawnContext::global()`]. + fn spawn_context_for_flow(&self, flow: &FlowDefinition) -> SpawnContext { + let Some(runtime) = self.projects.get(&flow.project) else { + return SpawnContext::global(); + }; + let working_dir_str = runtime.working_dir.to_string_lossy().into_owned(); + let mut ctx = SpawnContext::with_working_dir(runtime.working_dir.clone()) + .with_env("ADF_PROJECT_ID", flow.project.clone()) + .with_env("ADF_WORKING_DIR", working_dir_str); + if let Some(owner) = runtime.gitea_owner.as_deref() { + ctx = ctx.with_env("GITEA_OWNER", owner.to_string()); + } + if let Some(repo) = runtime.gitea_repo.as_deref() { + ctx = ctx.with_env("GITEA_REPO", repo.to_string()); } + ctx } /// Execute an action step (shell command). @@ -198,6 +238,7 @@ impl FlowExecutor { max_cpu_seconds: None, pre_check: None, gitea_issue: None, + project: None, }; // Build provider for spawner @@ -222,10 +263,11 @@ impl FlowExecutor { request = request.with_primary_model(model); } - // Spawn the agent + // Spawn the agent using the flow's project context + let spawn_ctx = self.spawn_context_for_flow(flow); let mut handle = self .spawner - .spawn_with_fallback(&request) + .spawn_with_fallback(&request, spawn_ctx) .await .map_err(|e| OrchestratorError::FlowFailed { flow_name: flow.name.clone(), @@ -527,6 +569,7 @@ mod tests { fn create_test_flow() -> FlowDefinition { FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/home/user/project".to_string(), base_branch: "develop".to_string(), @@ -744,6 +787,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -783,6 +827,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -820,6 +865,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -868,6 +914,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -918,6 +965,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -976,6 +1024,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -1040,6 +1089,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -1107,6 +1157,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -1186,6 +1237,7 @@ mod tests { let flow = FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: None, repo_path: "/tmp/repo".to_string(), base_branch: "main".to_string(), @@ -1273,6 +1325,7 @@ mod tests { let flow = FlowDefinition { name: "checkpoint-test".to_string(), + project: "test".to_string(), schedule: None, repo_path: dir.to_string_lossy().to_string(), base_branch: "main".to_string(), @@ -1353,6 +1406,7 @@ mod tests { let flow = FlowDefinition { name: "timeout-test".to_string(), + project: "test".to_string(), schedule: None, repo_path: dir.to_string_lossy().to_string(), base_branch: "main".to_string(), diff --git a/crates/terraphim_orchestrator/src/lib.rs b/crates/terraphim_orchestrator/src/lib.rs index e6f6bf161..5c7b178d7 100644 --- a/crates/terraphim_orchestrator/src/lib.rs +++ b/crates/terraphim_orchestrator/src/lib.rs @@ -38,6 +38,7 @@ pub mod cost_tracker; pub mod dispatcher; pub mod dual_mode; pub mod error; +pub mod error_signatures; pub mod flow; pub mod handoff; pub mod kg_router; @@ -48,6 +49,8 @@ pub mod mode; pub mod nightwatch; pub mod output_poster; pub mod persona; +pub mod project_control; +pub mod provider_budget; pub mod provider_probe; #[cfg(feature = "quickwit")] pub mod quickwit; @@ -73,7 +76,8 @@ pub use dual_mode::DualModeOrchestrator; pub use error::OrchestratorError; pub use handoff::{HandoffBuffer, HandoffContext, HandoffLedger}; pub use mention::{ - parse_mentions, resolve_mention, DetectedMention, MentionCursor, MentionTracker, + migrate_legacy_mention_cursor, parse_mention_tokens, parse_mentions, resolve_mention, + resolve_persona_mention, DetectedMention, MentionCursor, MentionTokens, MentionTracker, }; pub use metrics_persistence::{ InMemoryMetricsPersistence, MetricsPersistence, MetricsPersistenceConfig, @@ -101,7 +105,7 @@ use std::sync::{Arc, Mutex}; use terraphim_router::RoutingEngine; use terraphim_spawner::health::{CircuitBreaker, HealthStatus}; use terraphim_spawner::output::OutputEvent; -use terraphim_spawner::{AgentHandle, AgentSpawner, ResourceLimits, SpawnRequest}; +use terraphim_spawner::{AgentHandle, AgentSpawner, ResourceLimits, SpawnContext, SpawnRequest}; use tokio::sync::broadcast; use tracing::{debug, error, info, warn}; @@ -150,7 +154,17 @@ struct ManagedAgent { #[cfg(not(test))] #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] struct PersistedRestartState { + /// project -> agent -> restart count + #[serde(default)] + counts_by_project: HashMap>, + /// project -> agent -> last failure timestamp (unix seconds) + #[serde(default)] + last_failure_unix_secs_by_project: HashMap>, + /// Legacy flat map retained for backward-compatible deserialisation. + #[serde(default, skip_serializing)] counts: HashMap, + /// Legacy flat map retained for backward-compatible deserialisation. + #[serde(default, skip_serializing)] last_failure_unix_secs: HashMap, } @@ -165,12 +179,12 @@ pub struct AgentOrchestrator { active_agents: HashMap, rate_limiter: RateLimitTracker, shutdown_requested: bool, - /// Total restart count per agent (persists across agent lifecycle). - restart_counts: HashMap, - /// Last non-zero exit timestamp per agent (unix seconds), used for budget windowing. - restart_last_failure_unix_secs: HashMap, - /// Last exit time per agent (for cooldown enforcement). - restart_cooldowns: HashMap, + /// Total restart count per (project, agent) pair (persists across agent lifecycle). + restart_counts: HashMap<(String, String), u32>, + /// Last non-zero exit timestamp per (project, agent), used for budget windowing. + restart_last_failure_unix_secs: HashMap<(String, String), i64>, + /// Last exit time per (project, agent) (for cooldown enforcement). + restart_cooldowns: HashMap<(String, String), Instant>, /// Timestamp of the last reconciliation tick (for cron comparison). last_tick_time: chrono::DateTime, /// In-memory buffer for handoff contexts with TTL. @@ -196,14 +210,18 @@ pub struct AgentOrchestrator { /// Active flow executions keyed by flow name. #[allow(dead_code)] active_flows: HashMap>, - /// Tracker for processed @adf: mentions (dedup + depth limiting). - mention_cursor: Option, + /// Per-project mention cursors, keyed by project id. + /// + /// Each project gets its own cursor so repo-wide polls can advance + /// independently. Legacy single-project mode uses the synthetic + /// [`dispatcher::LEGACY_PROJECT_ID`] key. + mention_cursors: HashMap, /// Receiver for webhook dispatch requests. webhook_dispatch_rx: Option>, /// Monotonically increasing tick counter for poll_modulo gating. tick_count: u64, #[cfg(feature = "quickwit")] - quickwit_sink: Option, + quickwit_sink: Option, /// Classifier for structured agent exit classification using KG-boosted matching. exit_classifier: ExitClassifier, /// KG-driven model router loaded from taxonomy markdown files. @@ -212,6 +230,177 @@ pub struct AgentOrchestrator { provider_health: provider_probe::ProviderHealthMap, /// Live telemetry store for model performance tracking from CLI output. telemetry_store: control_plane::TelemetryStore, + /// Per-provider hour/day spend tracker consulted by the routing + /// engine. `None` when the config declares no [[providers]] entries. + provider_budget_tracker: Option>, + /// Compiled per-provider stderr classifiers built from + /// `[[providers]].error_signatures`. Providers without signatures + /// are absent from the map and classify as + /// [`error_signatures::ErrorKind::Unknown`] (fail-safe). + provider_error_signatures: error_signatures::ProviderSignatureMap, + /// Dedupe set of [`error_signatures::unknown_dedupe_key`] values so we + /// don't open a new `[ADF]` Gitea issue for every retry of the same + /// stderr shape. Process-lifetime in-memory; intentional since the + /// window between duplicates is short and restarting the orchestrator + /// is an acceptable dedupe reset. + unknown_error_dedupe: Arc>>, + /// Counter of consecutive `project-meta` failures per project. Tripped + /// entries cause the orchestrator to create a pause flag and open an + /// `[ADF]` Gitea escalation issue. + project_failure_counter: project_control::ProjectFailureCounter, + /// Resolved pause-flag directory. Derived from + /// [`OrchestratorConfig::pause_dir`] or [`project_control::DEFAULT_PAUSE_DIR`]. + pause_dir: PathBuf, +} + +/// Build the composite restart-state key for an agent definition. +/// +/// Legacy (project-less) agents use [`crate::dispatcher::LEGACY_PROJECT_ID`] +/// so restart counts never collide across projects once projects are added. +fn agent_key(def: &AgentDefinition) -> (String, String) { + ( + def.project + .clone() + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()), + def.name.clone(), + ) +} + +/// Build the optional provider-level hour/day budget tracker from the +/// [[providers]] config block. Returns `Ok(None)` when no providers are +/// declared (no cap = no tracker, routing works unchanged). +fn build_provider_budget_tracker( + config: &OrchestratorConfig, +) -> Result>, OrchestratorError> { + if config.providers.is_empty() { + if config.provider_budget_state_file.is_some() { + warn!("provider_budget_state_file set but no [[providers]] entries; tracker disabled"); + } + return Ok(None); + } + let tracker = match config.provider_budget_state_file.as_ref() { + Some(path) => provider_budget::ProviderBudgetTracker::with_persistence( + config.providers.clone(), + path.clone(), + ) + .map_err(|e| { + OrchestratorError::Config(format!( + "failed to load provider budget state from {}: {}", + path.display(), + e + )) + })?, + None => provider_budget::ProviderBudgetTracker::new(config.providers.clone()), + }; + info!( + providers = tracker.providers().collect::>().join(","), + persistence = ?config.provider_budget_state_file, + "provider budget tracker initialised" + ); + Ok(Some(Arc::new(tracker))) +} + +/// Build the per-project runtime map consumed by +/// [`flow::executor::FlowExecutor::with_projects`]. +fn build_flow_project_runtimes( + config: &OrchestratorConfig, +) -> HashMap { + config + .projects + .iter() + .map(|p| { + ( + p.id.clone(), + flow::executor::ProjectRuntime { + working_dir: p.working_dir.clone(), + gitea_owner: p.gitea.as_ref().map(|g| g.owner.clone()), + gitea_repo: p.gitea.as_ref().map(|g| g.repo.clone()), + }, + ) + }) + .collect() +} + +/// Build a [`SpawnContext`] for an agent, resolving per-project working_dir, +/// Gitea owner/repo, and the project id itself into the child process's +/// environment (`ADF_PROJECT_ID`, `ADF_WORKING_DIR`, `GITEA_OWNER`, +/// `GITEA_REPO`). Legacy (project-less) agents use [`SpawnContext::global()`]. +fn build_spawn_context_for_agent( + config: &OrchestratorConfig, + def: &AgentDefinition, +) -> SpawnContext { + let Some(pid) = def.project.as_deref() else { + return SpawnContext::global(); + }; + let Some(project) = config.project_by_id(pid) else { + return SpawnContext::global(); + }; + let working_dir_str = project.working_dir.to_string_lossy().into_owned(); + let mut ctx = SpawnContext::with_working_dir(project.working_dir.clone()) + .with_env("ADF_PROJECT_ID", pid) + .with_env("ADF_WORKING_DIR", working_dir_str); + if let Some(gitea) = project.gitea.as_ref() { + ctx = ctx + .with_env("GITEA_OWNER", gitea.owner.clone()) + .with_env("GITEA_REPO", gitea.repo.clone()); + } + ctx +} + +/// Flatten persisted nested maps (project -> agent -> count) and any legacy +/// flat entries into the in-memory composite key map. Legacy flat entries are +/// mapped to [`crate::dispatcher::LEGACY_PROJECT_ID`]. +#[cfg(not(test))] +fn flatten_restart_counts(state: &PersistedRestartState) -> HashMap<(String, String), u32> { + let mut out: HashMap<(String, String), u32> = HashMap::new(); + for (project, per_agent) in &state.counts_by_project { + for (agent, count) in per_agent { + out.insert((project.clone(), agent.clone()), *count); + } + } + for (agent, count) in &state.counts { + out.entry(( + crate::dispatcher::LEGACY_PROJECT_ID.to_string(), + agent.clone(), + )) + .or_insert(*count); + } + out +} + +/// Flatten persisted nested maps for last-failure timestamps into the +/// composite key format. +#[cfg(not(test))] +fn flatten_restart_failures(state: &PersistedRestartState) -> HashMap<(String, String), i64> { + let mut out: HashMap<(String, String), i64> = HashMap::new(); + for (project, per_agent) in &state.last_failure_unix_secs_by_project { + for (agent, ts) in per_agent { + out.insert((project.clone(), agent.clone()), *ts); + } + } + for (agent, ts) in &state.last_failure_unix_secs { + out.entry(( + crate::dispatcher::LEGACY_PROJECT_ID.to_string(), + agent.clone(), + )) + .or_insert(*ts); + } + out +} + +/// Re-nest composite keyed maps into the persisted `project -> agent -> value` +/// shape for serialisation. +#[cfg(not(test))] +fn nest_by_project( + flat: &HashMap<(String, String), V>, +) -> HashMap> { + let mut out: HashMap> = HashMap::new(); + for ((project, agent), value) in flat { + out.entry(project.clone()) + .or_default() + .insert(agent.clone(), value.clone()); + } + out } /// Validate agent name for safe use in file paths. @@ -300,8 +489,10 @@ impl AgentOrchestrator { None => MetapromptRenderer::new().expect("default template should always compile"), }; - // Initialize output poster if Gitea config is provided - let output_poster = config.gitea.as_ref().map(OutputPoster::new); + // Initialize output poster. In multi-project mode this wires one + // tracker per project plus a legacy fallback from `config.gitea`; in + // legacy single-project mode it collapses to the top-level config. + let output_poster = OutputPoster::from_orchestrator_config(&config); // Initialize KG router from taxonomy directory if configured let kg_router = config.routing.as_ref().and_then(|routing_config| { @@ -331,6 +522,22 @@ impl AgentOrchestrator { let telemetry_store = control_plane::TelemetryStore::new(3600); + let provider_budget_tracker = build_provider_budget_tracker(&config)?; + + // Compile per-provider stderr signatures declared under + // `[[providers]].error_signatures`. Invalid regexes fail loud + // at startup so misconfiguration can never silently disable + // runtime classification. + let provider_error_signatures = error_signatures::build_signature_map(&config.providers) + .map_err(|e| OrchestratorError::Config(e.to_string()))?; + + let project_failure_counter = + project_control::ProjectFailureCounter::new(config.project_circuit_breaker_threshold); + let pause_dir = config + .pause_dir + .clone() + .unwrap_or_else(|| PathBuf::from(project_control::DEFAULT_PAUSE_DIR)); + #[cfg(not(test))] let restart_state = Self::load_restart_state(); @@ -349,7 +556,7 @@ impl AgentOrchestrator { restart_counts: { #[cfg(not(test))] { - restart_state.counts.clone() + flatten_restart_counts(&restart_state) } #[cfg(test)] { @@ -359,7 +566,7 @@ impl AgentOrchestrator { restart_last_failure_unix_secs: { #[cfg(not(test))] { - restart_state.last_failure_unix_secs.clone() + flatten_restart_failures(&restart_state) } #[cfg(test)] { @@ -378,7 +585,7 @@ impl AgentOrchestrator { last_run_commits: HashMap::new(), pre_check_tracker: None, active_flows: HashMap::new(), - mention_cursor: None, + mention_cursors: HashMap::new(), webhook_dispatch_rx: None, tick_count: 0, #[cfg(feature = "quickwit")] @@ -387,6 +594,11 @@ impl AgentOrchestrator { kg_router, provider_health, telemetry_store, + provider_budget_tracker, + provider_error_signatures, + unknown_error_dedupe: Arc::new(Mutex::new(std::collections::HashSet::new())), + project_failure_counter, + pause_dir, }) } @@ -402,7 +614,7 @@ impl AgentOrchestrator { } else if let Ok(counts) = serde_json::from_str::>(&json) { PersistedRestartState { counts, - last_failure_unix_secs: HashMap::new(), + ..Default::default() } } else { PersistedRestartState::default() @@ -421,8 +633,12 @@ impl AgentOrchestrator { { let path = std::env::temp_dir().join("adf_restart_counts.json"); let state = PersistedRestartState { - counts: self.restart_counts.clone(), - last_failure_unix_secs: self.restart_last_failure_unix_secs.clone(), + counts_by_project: nest_by_project(&self.restart_counts), + last_failure_unix_secs_by_project: nest_by_project( + &self.restart_last_failure_unix_secs, + ), + counts: HashMap::new(), + last_failure_unix_secs: HashMap::new(), }; if let Ok(json) = serde_json::to_string(&state) { if let Err(e) = std::fs::write(&path, json) { @@ -436,29 +652,29 @@ impl AgentOrchestrator { self.config.restart_budget_window_secs as i64 } - fn current_restart_count(&mut self, name: &str) -> u32 { + fn current_restart_count(&mut self, key: &(String, String)) -> u32 { let now = chrono::Utc::now().timestamp(); let window = self.restart_budget_window_secs(); - let last_failure = self.restart_last_failure_unix_secs.get(name).copied(); + let last_failure = self.restart_last_failure_unix_secs.get(key).copied(); if let Some(last) = last_failure { if now.saturating_sub(last) > window { - self.restart_counts.remove(name); - self.restart_last_failure_unix_secs.remove(name); + self.restart_counts.remove(key); + self.restart_last_failure_unix_secs.remove(key); self.save_restart_state(); } } - self.restart_counts.get(name).copied().unwrap_or(0) + self.restart_counts.get(key).copied().unwrap_or(0) } - fn increment_restart_count(&mut self, name: &str) -> u32 { - let _ = self.current_restart_count(name); + fn increment_restart_count(&mut self, key: &(String, String)) -> u32 { + let _ = self.current_restart_count(key); let next_count = { - let count = self.restart_counts.entry(name.to_string()).or_insert(0); + let count = self.restart_counts.entry(key.clone()).or_insert(0); *count += 1; *count }; self.restart_last_failure_unix_secs - .insert(name.to_string(), chrono::Utc::now().timestamp()); + .insert(key.clone(), chrono::Utc::now().timestamp()); self.save_restart_state(); next_count } @@ -482,11 +698,21 @@ impl AgentOrchestrator { } /// Create from a TOML config file path. + /// + /// Loads the config, resolves include globs, and runs full validation + /// (banned providers, duplicate project ids, unknown project refs, mixed + /// mode). Returns `Err` if any check fails -- does not panic or warn-and- + /// continue. pub fn from_config_file(path: impl AsRef) -> Result { - let config = OrchestratorConfig::from_file(path)?; + let config = OrchestratorConfig::load_and_validate(path)?; Self::new(config) } + /// Return the validated configuration stored in this orchestrator. + pub fn config(&self) -> &OrchestratorConfig { + &self.config + } + /// Run the orchestrator (blocks until shutdown signal). /// /// 1. Spawns all Safety-layer agents immediately @@ -525,6 +751,11 @@ impl AgentOrchestrator { // Restore persisted telemetry from previous runs self.restore_telemetry().await; + // One-shot migration of the legacy top-level `adf/mention_cursor` + // key into per-project keys. No-op after the first successful + // startup. + mention::migrate_legacy_mention_cursor(&self.config.projects).await; + // Spawn Safety-layer agents immediately let immediate = self.scheduler.immediate_agents(); for agent_def in &immediate { @@ -597,16 +828,31 @@ impl AgentOrchestrator { for dispatch in pending_dispatches { self.handle_webhook_dispatch(dispatch).await; } - // Mark webhook-dispatched comments in the mention cursor so the - // poller skips them without needing another Gitea API call. + // Mark webhook-dispatched comments in every project cursor so + // each project's poller skips them without needing another + // Gitea API call. The webhook payload does not yet carry a + // project id, so we stamp all known cursors (plus the legacy + // fallback) to stay correct across both modes. if !webhook_comment_ids.is_empty() { - let cursor = self - .mention_cursor - .get_or_insert_with(mention::MentionCursor::now); - for cid in webhook_comment_ids { - cursor.mark_processed(cid); + let project_ids: Vec = if self.config.projects.is_empty() { + vec![dispatcher::LEGACY_PROJECT_ID.to_string()] + } else { + self.config.projects.iter().map(|p| p.id.clone()).collect() + }; + for pid in &project_ids { + let cursor = self + .mention_cursors + .entry(pid.clone()) + .or_insert_with(mention::MentionCursor::now); + for cid in &webhook_comment_ids { + cursor.mark_processed(*cid); + } + } + for pid in &project_ids { + if let Some(cursor) = self.mention_cursors.get(pid) { + cursor.save(pid).await; + } } - cursor.save().await; } tokio::select! { @@ -624,6 +870,11 @@ impl AgentOrchestrator { // Graceful shutdown of all agents self.persist_telemetry(); + if let Some(tracker) = self.provider_budget_tracker.as_ref() { + if let Err(e) = tracker.persist() { + warn!(error = %e, "failed to persist provider budget snapshot during shutdown"); + } + } self.shutdown_all_agents().await; Ok(()) } @@ -778,7 +1029,7 @@ impl AgentOrchestrator { } #[cfg(feature = "quickwit")] - pub fn set_quickwit_sink(&mut self, sink: quickwit::QuickwitSink) { + pub fn set_quickwit_sink(&mut self, sink: quickwit::QuickwitFleetSink) { self.quickwit_sink = Some(sink); } @@ -787,6 +1038,42 @@ impl AgentOrchestrator { self.config.quickwit.as_ref() } + /// Enumerate per-project Quickwit configurations plus a legacy fallback + /// for the top-level config, so the binary can build a + /// [`quickwit::QuickwitFleetSink`] covering every project. + /// + /// Returns `(project_id, QuickwitConfig)` pairs. Projects without a + /// per-project Quickwit block inherit the top-level config. The legacy + /// single-project path emits a single entry keyed on + /// [`crate::dispatcher::LEGACY_PROJECT_ID`]. + #[cfg(feature = "quickwit")] + pub fn quickwit_fleet_configs(&self) -> Vec<(String, QuickwitConfig)> { + let mut out: Vec<(String, QuickwitConfig)> = Vec::new(); + + for project in &self.config.projects { + if let Some(cfg) = project + .quickwit + .as_ref() + .or(self.config.quickwit.as_ref()) + .cloned() + { + if cfg.enabled { + out.push((project.id.clone(), cfg)); + } + } + } + + if self.config.projects.is_empty() { + if let Some(cfg) = self.config.quickwit.as_ref().cloned() { + if cfg.enabled { + out.push((crate::dispatcher::LEGACY_PROJECT_ID.to_string(), cfg)); + } + } + } + + out + } + /// Load skill chain content from skill definition files for the given agent definition. /// /// Reads each skill named in `def.skill_chain` from `{skill_data_dir}/{name}/SKILL.md`. @@ -914,6 +1201,21 @@ impl AgentOrchestrator { /// Otherwise, route the task prompt through the RoutingEngine to select /// a model based on keyword matching. async fn spawn_agent(&mut self, def: &AgentDefinition) -> Result<(), OrchestratorError> { + // === PROJECT PAUSE GATE === + // Operators and the project circuit breaker can block all dispatches + // for a given project by creating a sentinel file at + // `/`. The gate is project-scoped; legacy / + // global agents (`def.project == None`) are never blocked here. + if project_control::is_project_paused(&self.pause_dir, def.project.as_deref()) { + info!( + agent = %def.name, + project = ?def.project, + pause_dir = %self.pause_dir.display(), + "skipping spawn: project is paused" + ); + return Ok(()); + } + // === DISK SPACE GUARD === let threshold = self.config.disk_usage_threshold; if threshold < 100 { @@ -933,6 +1235,29 @@ impl AgentOrchestrator { } } + // === BUDGET GATE === + // Skip spawn entirely if the agent's monthly budget is exhausted. + // CostTracker::check is already called during routing (for budget + // pressure scoring), but routing only deprioritises cheaper models; + // it does not short-circuit dispatch. A fully exhausted agent must + // not run at all this cycle. + let budget_check = self.cost_tracker.check(&def.name); + if budget_check.should_pause() { + warn!( + agent = %def.name, + verdict = %budget_check, + "skipping spawn: monthly budget exhausted" + ); + return Ok(()); + } + if budget_check.should_warn() { + warn!( + agent = %def.name, + verdict = %budget_check, + "budget near exhaustion; routing will prefer cheaper models" + ); + } + // === PRE-CHECK GATE === let pre_check_result = self.run_pre_check(def).await; let findings = match pre_check_result { @@ -975,11 +1300,12 @@ impl AgentOrchestrator { .map(|r| std::sync::Arc::new(r.clone())); let unhealthy = self.provider_health.unhealthy_providers(); let telemetry_arc = std::sync::Arc::new(self.telemetry_store.clone()); - let engine = control_plane::RoutingDecisionEngine::new( + let engine = control_plane::RoutingDecisionEngine::with_provider_budget( kg_arc, unhealthy, terraphim_router::Router::new(), Some(telemetry_arc), + self.provider_budget_tracker.clone(), ); let ctx = control_plane::DispatchContext { agent_name: def.name.clone(), @@ -1239,9 +1565,10 @@ impl AgentOrchestrator { } request = request.with_resource_limits(limits); + let spawn_ctx = build_spawn_context_for_agent(&self.config, def); let handle = self .spawner - .spawn_with_fallback(&request) + .spawn_with_fallback(&request, spawn_ctx) .await .map_err(|e| OrchestratorError::SpawnFailed { agent: def.name.clone(), @@ -1252,7 +1579,11 @@ impl AgentOrchestrator { let output_rx = handle.subscribe_output(); // Get the restart count from the orchestrator-level counter - let restart_count = self.restart_counts.get(&def.name).copied().unwrap_or(0); + let restart_count = self + .restart_counts + .get(&agent_key(def)) + .copied() + .unwrap_or(0); self.active_agents.insert( def.name.clone(), @@ -1278,6 +1609,10 @@ impl AgentOrchestrator { if let Some(ref sink) = self.quickwit_sink { let doc = quickwit::LogDocument { timestamp: chrono::Utc::now().to_rfc3339(), + project_id: def + .project + .clone() + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()), level: "INFO".into(), agent_name: def.name.clone(), layer: format!("{:?}", def.layer), @@ -1599,18 +1934,28 @@ impl AgentOrchestrator { match dispatch { webhook::WebhookDispatch::SpawnAgent { agent_name, + detected_project, issue_number, comment_id, context, } => { info!( agent = %agent_name, + project = ?detected_project, issue = issue_number, comment_id = comment_id, "webhook: dispatching agent spawn" ); - if let Some(def) = agents.iter().find(|a| a.name == agent_name).cloned() { + // Use project-aware resolver. For webhook dispatches we don't know which + // project's repo the webhook came from, so we use LEGACY_PROJECT_ID as the + // hint for unqualified mentions; qualified mentions carry detected_project. + if let Some(def) = mention::resolve_mention( + detected_project.as_deref(), + dispatcher::LEGACY_PROJECT_ID, + &agent_name, + &agents, + ) { // Dedup: check Gitea assignment + active_agents before spawning if self.should_skip_dispatch(&agent_name, issue_number).await { return; @@ -1636,7 +1981,7 @@ impl AgentOrchestrator { comment_id, context, } => { - if let Some((agent_name, _)) = mention::resolve_mention( + if let Some((agent_name, _)) = mention::resolve_persona_mention( &persona_name, &agents, &self.persona_registry, @@ -1702,32 +2047,102 @@ impl AgentOrchestrator { /// (no persisted cursor), cursor is set to `now` to skip all historical /// mentions — preventing the mention replay storm. async fn poll_mentions(&mut self) { - let mention_cfg = match self.config.mentions.clone() { - Some(cfg) => cfg, - None => return, - }; + // Build the list of (project_id, gitea_cfg, mention_cfg) targets. + // + // - Legacy mode (no `[[projects]]`): one pass under the synthetic + // `__global__` id using the top-level `gitea` and `mentions`. + // - Multi-project mode: one pass per configured project that + // declares a `gitea` block. Per-project `mentions` override the + // top-level `mentions`, which in turn falls back to + // `MentionConfig::default()` so operators need not repeat caps + // in every project. + let targets: Vec<(String, config::GiteaOutputConfig, config::MentionConfig)> = + if self.config.projects.is_empty() { + match (self.config.mentions.clone(), self.config.gitea.clone()) { + (Some(m), Some(g)) => { + vec![(dispatcher::LEGACY_PROJECT_ID.to_string(), g, m)] + } + _ => { + tracing::debug!( + "mention polling skipped: legacy mode but no Gitea/mentions config" + ); + return; + } + } + } else { + let global_mentions = self.config.mentions.clone(); + self.config + .projects + .iter() + .filter_map(|project| { + if project.gitea.is_none() { + tracing::debug!( + project = project.id.as_str(), + "skipping mention poll: project has no gitea config" + ); + } + let gitea = project.gitea.clone()?; + let mentions = project + .mentions + .clone() + .or_else(|| global_mentions.clone()) + .unwrap_or_default(); + Some((project.id.clone(), gitea, mentions)) + }) + .collect() + }; - let gitea_cfg = match self.config.gitea.clone() { - Some(cfg) => cfg, - None => { - tracing::debug!("mention polling skipped: no Gitea output config"); - return; - } - }; + if targets.is_empty() { + tracing::debug!("mention polling skipped: no projects with Gitea config"); + return; + } - // Respect poll_modulo to reduce API traffic + for (project_id, gitea_cfg, mention_cfg) in targets { + self.poll_mentions_for_project(&project_id, &gitea_cfg, &mention_cfg) + .await; + } + } + + /// Run a single mention-poll pass for one project. + /// + /// Invoked by [`AgentOrchestrator::poll_mentions`] for each configured + /// project (or once for legacy single-project mode under + /// `__global__`). Loads/persists the project's cursor, honours the + /// project's `MentionConfig`, and threads `project_id` onto every + /// dispatched mention. + async fn poll_mentions_for_project( + &mut self, + project_id: &str, + gitea_cfg: &config::GiteaOutputConfig, + mention_cfg: &config::MentionConfig, + ) { + // Respect poll_modulo to reduce API traffic. if self.tick_count % mention_cfg.poll_modulo != 0 { return; } - // Count currently active mention-spawned agents - let active_mention_agents = self - .active_agents - .values() - .filter(|a| a.spawned_by_mention) - .count() as u32; + // Count currently active mention-spawned agents for this project. + // + // We filter by the agent definition's project field so one noisy + // project cannot exhaust the fleet-wide mention budget for others. + // In legacy mode (project_id == "__global__") every agent + // contributes because project binding isn't meaningful there. + let active_mention_agents = if project_id == dispatcher::LEGACY_PROJECT_ID { + self.active_agents + .values() + .filter(|a| a.spawned_by_mention) + .count() as u32 + } else { + self.active_agents + .values() + .filter(|a| { + a.spawned_by_mention && a.definition.project.as_deref() == Some(project_id) + }) + .count() as u32 + }; if active_mention_agents >= mention_cfg.max_concurrent_mention_agents { tracing::debug!( + project = project_id, active = active_mention_agents, max = mention_cfg.max_concurrent_mention_agents, "mention agents at capacity, skipping poll" @@ -1735,10 +2150,10 @@ impl AgentOrchestrator { return; } - // Lazy-load cursor on first poll - let mut cursor = match self.mention_cursor.take() { + // Lazy-load the project's cursor. + let mut cursor = match self.mention_cursors.remove(project_id) { Some(c) => c, - None => mention::MentionCursor::load_or_now().await, + None => mention::MentionCursor::load_or_now(project_id).await, }; cursor.dispatches_this_tick = 0; @@ -1757,8 +2172,12 @@ impl AgentOrchestrator { let tracker = match terraphim_tracker::GiteaTracker::new(tracker_cfg) { Ok(t) => t, Err(e) => { - tracing::warn!(error = %e, "failed to create GiteaTracker for mention polling"); - self.mention_cursor = Some(cursor); + tracing::warn!( + project = project_id, + error = %e, + "failed to create GiteaTracker for mention polling" + ); + self.mention_cursors.insert(project_id.to_string(), cursor); return; } }; @@ -1770,15 +2189,19 @@ impl AgentOrchestrator { { Ok(c) => c, Err(e) => { - tracing::warn!(error = %e, "failed to fetch repo comments for mention polling"); - self.mention_cursor = Some(cursor); + tracing::warn!( + project = project_id, + error = %e, + "failed to fetch repo comments for mention polling" + ); + self.mention_cursors.insert(project_id.to_string(), cursor); return; } }; if comments.is_empty() { - cursor.save().await; - self.mention_cursor = Some(cursor); + cursor.save(project_id).await; + self.mention_cursors.insert(project_id.to_string(), cursor); return; } @@ -1821,6 +2244,62 @@ impl AgentOrchestrator { let commands = command_parser.parse_commands(&comment.body, comment.issue_number, comment.id); + // Handle qualified `@adf:project/name` mentions that AdfCommandParser cannot + // see (its patterns are `@adf:{name}`; a `project/` prefix is not a substring). + for token in mention::parse_mention_tokens(&comment.body) { + if cursor.dispatches_this_tick >= max_dispatches { + break; + } + let proj = match token.project.as_deref() { + Some(p) => p, + None => continue, // unqualified mentions are handled by parse_commands below + }; + match mention::resolve_mention(Some(proj), project_id, &token.agent, &agents) { + Some(def) => { + info!( + agent = %token.agent, + project = proj, + issue = comment.issue_number, + comment_id = comment.id, + "dispatching qualified mention-driven agent" + ); + if self + .should_skip_dispatch(&token.agent, comment.issue_number) + .await + { + cursor.dispatches_this_tick += 1; + continue; + } + let mut mention_def = def.clone(); + mention_def.task = format!( + "{}\n\n## Mention Context\nTriggered by @adf:{}/{} mention in issue #{} (comment {}).", + def.task, proj, token.agent, comment.issue_number, comment.id + ); + mention_def.gitea_issue = Some(comment.issue_number); + if let Err(e) = self.spawn_agent(&mention_def).await { + tracing::error!( + agent = %token.agent, + project = proj, + issue = comment.issue_number, + error = %e, + "failed to spawn agent for qualified mention" + ); + } else if let Some(active) = self.active_agents.get_mut(&mention_def.name) { + active.spawned_by_mention = true; + } + cursor.dispatches_this_tick += 1; + } + None => { + tracing::warn!( + mention = format!("@adf:{}/{}", proj, token.agent), + project = project_id, + issue = comment.issue_number, + "qualified mention matched no agent" + ); + } + } + } + for cmd in commands { if cursor.dispatches_this_tick >= max_dispatches { break; @@ -1872,7 +2351,11 @@ impl AgentOrchestrator { "dispatching mention-driven agent via terraphim-automata parser" ); - if let Some(def) = agents.iter().find(|a| a.name == agent_name).cloned() { + // Unqualified mention: detected_project is None; use hinted project_id + // from the current poll context for multi-project resolution. + if let Some(def) = + mention::resolve_mention(None, project_id, &agent_name, &agents) + { // Dedup: check Gitea assignment + active_agents before spawning if self.should_skip_dispatch(&agent_name, issue_number).await { cursor.dispatches_this_tick += 1; @@ -1908,7 +2391,7 @@ impl AgentOrchestrator { context, } => { // Resolve persona to agent - if let Some((agent_name, _)) = mention::resolve_mention( + if let Some((agent_name, _)) = mention::resolve_persona_mention( &persona_name, &agents, &persona_registry, @@ -1964,8 +2447,8 @@ impl AgentOrchestrator { } // Persist cursor for next poll / restart - cursor.save().await; - self.mention_cursor = Some(cursor); + cursor.save(project_id).await; + self.mention_cursors.insert(project_id.to_string(), cursor); } /// Check if an agent is already assigned to this issue and currently active. @@ -1999,7 +2482,23 @@ impl AgentOrchestrator { let Some(ref poster) = self.output_poster else { return false; }; - let tracker = poster.tracker_for(agent_name); + // Resolve the agent's owning project so the tracker uses the + // correct owner/repo (multi-project) or falls back to legacy. + let project = self + .config + .agents + .iter() + .find(|a| a.name == agent_name) + .and_then(|a| a.project.clone()) + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()); + let Some(tracker) = poster.tracker_for(&project, agent_name) else { + warn!( + agent = %agent_name, + project = %project, + "no Gitea tracker for project; treating dispatch as not-duplicate" + ); + return false; + }; // Remote check: if agent is assigned in Gitea but not active (crash recovery) match tracker.fetch_issue_assignees(issue_number).await { @@ -2579,6 +3078,16 @@ impl AgentOrchestrator { if self.tick_count % 60 == 0 { self.persist_telemetry(); } + + // 16. Flush provider-budget snapshot. The tracker accumulates in + // memory via record_telemetry; persist here so hour/day counters + // carry across restarts (cf. with_persistence at construction). + // Skip when no tracker was configured. + if let Some(tracker) = self.provider_budget_tracker.as_ref() { + if let Err(e) = tracker.persist() { + warn!(error = %e, "failed to persist provider budget snapshot"); + } + } } /// Check all agent budgets and pause any that have exceeded their limits. @@ -2725,8 +3234,9 @@ impl AgentOrchestrator { } // Handle exit based on layer (similar to handle_agent_exit but for timeout) if managed.definition.layer == AgentLayer::Safety { - let restart_count = self.increment_restart_count(&name); - self.restart_cooldowns.insert(name.clone(), Instant::now()); + let key = agent_key(&managed.definition); + let restart_count = self.increment_restart_count(&key); + self.restart_cooldowns.insert(key, Instant::now()); info!( agent = %name, restart_count, @@ -2848,16 +3358,78 @@ impl AgentOrchestrator { } _ => {} // Other exit classes don't affect provider health } + + // issue #7: per-provider stderr-signature classification. This + // runs on top of the KG-driven ExitClass match above so that + // providers whose CLI exits 0 on quota hits ("returning partial + // output") still trip the breaker and force budget exhaustion + // when their stderr matches a configured throttle pattern. + let sigs = self.provider_error_signatures.get(provider); + let sig_kind = error_signatures::classify_lines(&stderr_lines, sigs); + match sig_kind { + error_signatures::ErrorKind::Throttle => { + warn!( + agent = %name, + provider = %provider, + model = ?record.model_used, + "stderr classified as throttle; tripping breaker + exhausting budget" + ); + self.provider_health.record_failure(provider); + if let Some(tracker) = self.provider_budget_tracker.as_ref() { + tracker.force_exhaust(provider); + } + } + error_signatures::ErrorKind::Flake => { + info!( + agent = %name, + provider = %provider, + "stderr classified as flake; routing will retry next pool entry" + ); + } + error_signatures::ErrorKind::Unknown => { + // Only escalate when there *is* stderr text to classify + // AND the run itself looks like a real failure. A clean + // exit with empty stderr must never page fleet-meta. + let looked_like_failure = !matches!( + record.exit_class, + ExitClass::Success | ExitClass::EmptySuccess + ); + if looked_like_failure && !stderr_lines.is_empty() { + // Soft-failure accounting so a pathological provider + // that spews unclassified errors still eventually + // opens the breaker. + self.provider_health.record_failure(provider); + self.escalate_unknown_error( + provider, + record.model_used.as_deref(), + &stderr_lines, + ) + .await; + } + } + } } - // Post output to Gitea if configured + // Post output to Gitea if configured, routed by the agent's + // owning project so multi-project fleets land comments in the + // correct owner/repo. if let (Some(poster), Some(issue)) = (&self.output_poster, def.gitea_issue) { let exit_code = status.code(); + let project = def + .project + .clone() + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()); if let Err(e) = poster - .post_agent_output(name, issue, &output_lines, exit_code) + .post_agent_output_for_project(&project, name, issue, &output_lines, exit_code) .await { - warn!(agent = %name, issue = issue, error = %e, "failed to post output to Gitea"); + warn!( + agent = %name, + project = %project, + issue = issue, + error = %e, + "failed to post output to Gitea" + ); } } @@ -2871,6 +3443,10 @@ impl AgentOrchestrator { }; let doc = quickwit::LogDocument { timestamp: chrono::Utc::now().to_rfc3339(), + project_id: def + .project + .clone() + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()), level: level.into(), agent_name: name.clone(), layer: format!("{:?}", def.layer), @@ -2906,6 +3482,7 @@ impl AgentOrchestrator { .and_then(|m| m.worktree_path.clone()); self.active_agents.remove(&name); self.handle_agent_exit(&name, &def, status); + self.record_project_meta_exit(&def, status).await; // Auto-commit in the agent's working directory (worktree or shared) let commit_dir = worktree_path.as_deref().unwrap_or(&self.config.working_dir); @@ -2920,6 +3497,157 @@ impl AgentOrchestrator { } } + /// Feed a `project-meta` exit into the per-project circuit breaker. On + /// the trip transition (Nth consecutive failure) this touches the pause + /// flag and opens an `[ADF]` escalation issue on the configured + /// fleet-escalation repository. + async fn record_project_meta_exit( + &mut self, + def: &AgentDefinition, + status: std::process::ExitStatus, + ) { + if !project_control::is_project_meta_agent(def) { + return; + } + let Some(project_id) = def.project.clone() else { + return; + }; + let success = status.success(); + let verdict = self + .project_failure_counter + .record_project_meta_result(&project_id, success); + let count = self.project_failure_counter.count(&project_id); + let threshold = self.project_failure_counter.threshold(); + info!( + project = %project_id, + agent = %def.name, + success, + consecutive_failures = count, + threshold, + "project-meta exit recorded" + ); + if verdict != project_control::ShouldPause::Yes { + return; + } + self.trip_project_circuit_breaker(&project_id, &def.name, count, threshold) + .await; + } + + /// Touch the pause flag and open an `[ADF]` escalation issue for a + /// project that has exceeded the `project-meta` failure threshold. + async fn trip_project_circuit_breaker( + &self, + project_id: &str, + agent_name: &str, + consecutive_failures: u32, + threshold: u32, + ) { + match project_control::touch_pause_flag(&self.pause_dir, project_id) { + Ok(path) => { + warn!( + project = %project_id, + pause_flag = %path.display(), + consecutive_failures, + threshold, + "project circuit breaker tripped; pause flag created" + ); + } + Err(e) => { + error!( + project = %project_id, + pause_dir = %self.pause_dir.display(), + error = %e, + "failed to create project pause flag" + ); + } + } + + let (Some(owner), Some(repo)) = self.fleet_escalation_target() else { + warn!( + project = %project_id, + "no fleet-escalation owner/repo configured; skipping issue creation" + ); + return; + }; + let Some(gitea_cfg) = self.config.gitea.as_ref() else { + warn!( + project = %project_id, + "no top-level gitea config; cannot open escalation issue" + ); + return; + }; + let tracker_cfg = terraphim_tracker::GiteaConfig { + base_url: gitea_cfg.base_url.clone(), + token: gitea_cfg.token.clone(), + owner: owner.clone(), + repo: repo.clone(), + active_states: vec!["open".to_string()], + terminal_states: vec!["closed".to_string()], + use_robot_api: false, + robot_path: std::path::PathBuf::from("/home/alex/go/bin/gitea-robot"), + claim_strategy: terraphim_tracker::gitea::ClaimStrategy::PreferRobot, + }; + let tracker = match terraphim_tracker::GiteaTracker::new(tracker_cfg) { + Ok(t) => t, + Err(e) => { + error!( + project = %project_id, + owner = %owner, + repo = %repo, + error = %e, + "failed to construct escalation GiteaTracker" + ); + return; + } + }; + let title = format!( + "[ADF] project-meta on {project_id} failed {consecutive_failures} consecutively" + ); + let body = format!( + "Project `{project_id}` has been paused by the circuit breaker after \ +{consecutive_failures} consecutive `project-meta` failures (threshold: {threshold}).\n\n\ +Agent: `{agent_name}`\n\n\ +Pause flag: `{pause}`\n\n\ +Remove the pause flag once the underlying failure is resolved:\n\n\ +```\nrm {pause}\n```\n", + pause = self.pause_dir.join(project_id).display() + ); + let labels = ["adf", "circuit-breaker", "priority/high"]; + match tracker.create_issue(&title, &body, &labels).await { + Ok(issue) => info!( + project = %project_id, + owner = %owner, + repo = %repo, + issue = issue.number, + "opened [ADF] escalation issue" + ), + Err(e) => error!( + project = %project_id, + owner = %owner, + repo = %repo, + error = %e, + "failed to open [ADF] escalation issue" + ), + } + } + + /// Resolve the `(owner, repo)` pair for fleet-level escalation issues. + /// Falls back to the configured Gitea output target when the dedicated + /// `fleet_escalation_*` fields are unset. + fn fleet_escalation_target(&self) -> (Option, Option) { + let owner = self + .config + .fleet_escalation_owner + .clone() + .or_else(|| self.config.gitea.as_ref().map(|g| g.owner.clone())); + let repo = self + .config + .fleet_escalation_repo + .clone() + .or_else(|| self.config.gitea.as_ref().map(|g| g.repo.clone())); + (owner, repo) + } + /// Handle an agent exit based on its layer. fn handle_agent_exit( &mut self, @@ -2927,15 +3655,15 @@ impl AgentOrchestrator { def: &AgentDefinition, status: std::process::ExitStatus, ) { + let key = agent_key(def); match def.layer { AgentLayer::Safety => { // Only count non-zero exits toward restart limit. // A successful exit (code 0) means the agent completed its task; // punishing it for succeeding makes no sense. if !status.success() { - let restart_count = self.increment_restart_count(name); - self.restart_cooldowns - .insert(name.to_string(), Instant::now()); + let restart_count = self.increment_restart_count(&key); + self.restart_cooldowns.insert(key, Instant::now()); if restart_count <= self.config.max_restart_count { info!( agent = %name, @@ -2955,8 +3683,7 @@ impl AgentOrchestrator { ); } } else { - self.restart_cooldowns - .insert(name.to_string(), Instant::now()); + self.restart_cooldowns.insert(key, Instant::now()); info!( agent = %name, exit_status = %status, @@ -2980,15 +3707,15 @@ impl AgentOrchestrator { let max_restarts = self.config.max_restart_count; // Age out stale restart counters before restart eligibility checks. - let safety_names: Vec = self + let safety_keys: Vec<(String, String)> = self .config .agents .iter() .filter(|def| def.layer == AgentLayer::Safety) - .map(|def| def.name.clone()) + .map(agent_key) .collect(); - for name in &safety_names { - let _ = self.current_restart_count(name); + for key in &safety_keys { + let _ = self.current_restart_count(key); } // Find Safety agents that need restarting @@ -3005,8 +3732,9 @@ impl AgentOrchestrator { if self.active_agents.contains_key(&def.name) { return false; } + let key = agent_key(def); // Must have a restart cooldown entry (meaning it exited) - let last_exit = match self.restart_cooldowns.get(&def.name) { + let last_exit = match self.restart_cooldowns.get(&key) { Some(t) => t, None => return false, }; @@ -3015,16 +3743,17 @@ impl AgentOrchestrator { return false; } // Must be under max restart count - let count = self.restart_counts.get(&def.name).copied().unwrap_or(0); + let count = self.restart_counts.get(&key).copied().unwrap_or(0); count <= max_restarts }) .cloned() .collect(); for def in to_restart { + let key = agent_key(&def); info!( agent = %def.name, - restart_count = self.restart_counts.get(&def.name).copied().unwrap_or(0), + restart_count = self.restart_counts.get(&key).copied().unwrap_or(0), "restarting safety agent after cooldown" ); if let Err(e) = self.spawn_agent(&def).await { @@ -3171,6 +3900,11 @@ impl AgentOrchestrator { .get(name) .map(|m| format!("{:?}", m.definition.layer)) .unwrap_or_default(); + let project_id = self + .active_agents + .get(name) + .and_then(|m| m.definition.project.clone()) + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()); let model = self.active_agents.get(name).and_then(|m| { m.routed_model .clone() @@ -3178,6 +3912,7 @@ impl AgentOrchestrator { }); let doc = quickwit::LogDocument { timestamp: chrono::Utc::now().to_rfc3339(), + project_id, level: level.into(), agent_name: name.clone(), layer, @@ -3193,19 +3928,43 @@ impl AgentOrchestrator { completion_events } - /// Record parsed telemetry events into the telemetry store and cost tracker. + /// Record parsed telemetry events into the telemetry store, per-agent + /// cost tracker, and (when configured) the provider-level hour/day + /// budget tracker. /// /// Cost accounting is performed per-agent before the batch write so that /// agent-level spend is still tracked individually. The telemetry store - /// write uses a single lock acquisition via `record_batch`. + /// write uses a single lock acquisition via `record_batch`. Provider + /// budget spend is folded in during the same iteration so Layer 3 of + /// the subscription gate actually sees real dispatch cost. async fn record_telemetry( &self, events: Vec<(String, control_plane::telemetry::CompletionEvent)>, ) { - // Record costs per-agent first (no lock involved). for (agent_name, event) in &events { if event.cost_usd > 0.0 { self.cost_tracker.record_cost(agent_name, event.cost_usd); + + if let Some(tracker) = self.provider_budget_tracker.as_ref() { + if let Some(provider_key) = + provider_budget::provider_key_for_model(&event.model) + { + let verdict = tracker.record_cost(provider_key, event.cost_usd); + if matches!( + verdict, + cost_tracker::BudgetVerdict::Exhausted { .. } + | cost_tracker::BudgetVerdict::NearExhaustion { .. } + ) { + warn!( + provider = provider_key, + agent = agent_name.as_str(), + cost_usd = event.cost_usd, + verdict = ?verdict, + "provider budget pressure recorded" + ); + } + } + } } } // Write all events in one lock acquisition. @@ -3409,13 +4168,15 @@ impl AgentOrchestrator { .clone() .unwrap_or_else(|| PathBuf::from("/tmp/flow-states")); let working_dir = self.config.compound_review.repo_path.clone(); + let project_runtimes = build_flow_project_runtimes(&self.config); let flow_def = *flow_def; let flow_name_for_closure = flow_name.clone(); // FlowExecutor contains non-Send types (Regex via AgentSpawner), // so we use spawn_blocking + Handle::block_on as a Send-safe bridge. let rt_handle = tokio::runtime::Handle::current(); let handle = tokio::task::spawn_blocking(move || { - let executor = flow::executor::FlowExecutor::new(working_dir, flow_state_dir); + let executor = flow::executor::FlowExecutor::new(working_dir, flow_state_dir) + .with_projects(project_runtimes); rt_handle.block_on(async { executor.run(&flow_def, None).await .unwrap_or_else(|e| { @@ -3535,6 +4296,162 @@ impl AgentOrchestrator { pub fn telemetry_store(&self) -> &control_plane::TelemetryStore { &self.telemetry_store } + + /// Test helper: access the provider budget tracker (if any). + #[doc(hidden)] + pub fn provider_budget_tracker(&self) -> Option<&Arc> { + self.provider_budget_tracker.as_ref() + } + + /// Test helper: inspect the unknown-error dedupe set (lock + clone). + #[doc(hidden)] + pub fn unknown_error_dedupe_snapshot(&self) -> std::collections::HashSet { + self.unknown_error_dedupe + .lock() + .map(|g| g.clone()) + .unwrap_or_default() + } + + /// Open a `[ADF] unknown error signature on /` Gitea + /// issue so fleet-meta can classify the pattern. Deduped by + /// [`error_signatures::unknown_dedupe_key`] within the process lifetime + /// so retries of the same stderr shape don't spam the tracker. + /// + /// The target tracker is the orchestrator's default [`GiteaTracker`] + /// from [`OutputPoster::tracker`], which points at the fleet-meta repo + /// configured in `orchestrator.toml`. If no `OutputPoster` is wired + /// (tests, legacy configs), this is a no-op. + async fn escalate_unknown_error( + &self, + provider: &str, + model: Option<&str>, + stderr_lines: &[String], + ) { + let joined = stderr_lines.join("\n"); + let dedupe_key = error_signatures::unknown_dedupe_key(provider, &joined); + { + let mut set = match self.unknown_error_dedupe.lock() { + Ok(g) => g, + Err(_) => { + warn!( + provider = %provider, + "unknown_error_dedupe lock poisoned; skipping escalation" + ); + return; + } + }; + if !set.insert(dedupe_key.clone()) { + // Already escalated this shape in this process. Skip quietly. + return; + } + } + + let Some(poster) = self.output_poster.as_ref() else { + info!( + provider = %provider, + dedupe_key = %dedupe_key, + "no output_poster configured; unknown stderr logged only" + ); + return; + }; + + // Cap stderr in the body so a runaway CLI never posts megabytes. + const MAX_STDERR_CHARS: usize = 4000; + let truncated: String = if joined.len() > MAX_STDERR_CHARS { + format!( + "{}\n...[truncated, original {} chars]", + joined.chars().take(MAX_STDERR_CHARS).collect::(), + joined.len() + ) + } else { + joined + }; + let model_slug = model.unwrap_or(""); + let title = format!( + "[ADF] unknown error signature on {}/{}", + provider, model_slug + ); + let body = format!( + "A spawned agent produced stderr that matched neither the \ + throttle nor the flake regex lists for provider `{}` \ + (model `{}`). Please review and extend the provider's \ + `error_signatures` config so future occurrences classify \ + correctly.\n\n\ + **Dedupe key:** `{}`\n\n\ + ## Captured stderr\n\n```\n{}\n```\n", + provider, model_slug, dedupe_key, truncated + ); + let labels = ["adf", "error-signature", "triage"]; + let tracker = poster.tracker(); + if let Err(e) = tracker.create_issue(&title, &body, &labels).await { + warn!( + provider = %provider, + model = %model_slug, + error = %e, + "failed to escalate unknown-error signature to Gitea" + ); + } else { + info!( + provider = %provider, + model = %model_slug, + dedupe_key = %dedupe_key, + "escalated unknown error signature to fleet-meta" + ); + } + } + + /// Test helper: drive `record_telemetry` from outside the crate so + /// integration tests can verify the cost-tracker / provider-budget + /// wiring without starting the full reconcile loop. + #[doc(hidden)] + pub async fn record_telemetry_for_test( + &self, + events: Vec<(String, control_plane::telemetry::CompletionEvent)>, + ) { + self.record_telemetry(events).await + } + + /// Test helper: return the pause directory the orchestrator is using. + #[doc(hidden)] + pub fn pause_dir_for_test(&self) -> &std::path::Path { + &self.pause_dir + } + + /// Test helper: drive the project circuit breaker from outside the crate + /// so integration tests can verify the trip → pause-flag path without + /// spawning real agent processes. + /// + /// Simulates recording `failure_count` consecutive `project-meta` failures + /// against `project_id`; on the Nth failure (where N == threshold) the + /// underlying counter trips and this function touches the pause flag. + /// Returns `true` iff the pause flag was (re)created by this call. + #[doc(hidden)] + pub async fn simulate_project_meta_failures_for_test( + &mut self, + project_id: &str, + failure_count: u32, + ) -> bool { + let mut tripped = false; + for _ in 0..failure_count { + let verdict = self + .project_failure_counter + .record_project_meta_result(project_id, false); + if verdict == project_control::ShouldPause::Yes { + let _ = project_control::touch_pause_flag(&self.pause_dir, project_id); + tripped = true; + } + } + tripped + } + + /// Test helper: record a successful project-meta run, resetting the + /// per-project consecutive-failure counter. + #[doc(hidden)] + pub fn reset_project_meta_counter_for_test(&mut self, project_id: &str) { + let _ = self + .project_failure_counter + .record_project_meta_result(project_id, true); + } } /// Check whether any changed file matches any of the watch path prefixes. @@ -3554,6 +4471,13 @@ mod tests { use super::*; use tempfile::TempDir; + fn legacy_key(name: &str) -> (String, String) { + ( + crate::dispatcher::LEGACY_PROJECT_ID.to_string(), + name.to_string(), + ) + } + fn test_config() -> OrchestratorConfig { OrchestratorConfig { working_dir: std::path::PathBuf::from("/tmp/test-orchestrator"), @@ -3595,6 +4519,8 @@ mod tests { pre_check: None, gitea_issue: None, + + project: None, }, AgentDefinition { name: "sync".to_string(), @@ -3618,6 +4544,8 @@ mod tests { pre_check: None, gitea_issue: None, + + project: None, }, ], restart_cooldown_secs: 60, @@ -3635,6 +4563,16 @@ mod tests { webhook: None, role_config_path: None, routing: None, + #[cfg(feature = "quickwit")] + quickwit: None, + projects: vec![], + include: vec![], + providers: vec![], + provider_budget_state_file: None, + pause_dir: None, + project_circuit_breaker_threshold: 3, + fleet_escalation_owner: None, + fleet_escalation_repo: None, } } @@ -3837,6 +4775,8 @@ task = "test" pre_check: None, gitea_issue: None, + + project: None, }], restart_cooldown_secs: 0, // instant restart for testing max_restart_count: 3, @@ -3853,6 +4793,16 @@ task = "test" webhook: None, role_config_path: None, routing: None, + #[cfg(feature = "quickwit")] + quickwit: None, + projects: vec![], + include: vec![], + providers: vec![], + provider_budget_state_file: None, + pause_dir: None, + project_circuit_breaker_threshold: 3, + fleet_escalation_owner: None, + fleet_escalation_repo: None, } } @@ -3880,7 +4830,10 @@ task = "test" // Successful exit (code 0) should NOT increment restart count assert_eq!( - orch.restart_counts.get("echo-safety").copied().unwrap_or(0), + orch.restart_counts + .get(&legacy_key("echo-safety")) + .copied() + .unwrap_or(0), 0, "successful exit should not increment restart count" ); @@ -3931,6 +4884,8 @@ task = "test" pre_check: None, gitea_issue: None, + + project: None, }]; let mut orch = AgentOrchestrator::new(config).unwrap(); @@ -3978,7 +4933,10 @@ task = "test" !orch.active_agents.contains_key("echo-safety"), "agent should not restart after exceeding max_restart_count" ); - assert_eq!(orch.restart_counts.get("echo-safety").copied(), Some(3)); + assert_eq!( + orch.restart_counts.get(&legacy_key("echo-safety")).copied(), + Some(3) + ); } #[test] @@ -3987,18 +4945,18 @@ task = "test" config.restart_budget_window_secs = 1; let mut orch = AgentOrchestrator::new(config).unwrap(); - orch.restart_counts.insert("echo-safety".to_string(), 3); + orch.restart_counts.insert(legacy_key("echo-safety"), 3); orch.restart_last_failure_unix_secs.insert( - "echo-safety".to_string(), + legacy_key("echo-safety"), chrono::Utc::now().timestamp() - 5, ); - let count = orch.current_restart_count("echo-safety"); + let count = orch.current_restart_count(&legacy_key("echo-safety")); assert_eq!(count, 0); - assert!(!orch.restart_counts.contains_key("echo-safety")); + assert!(!orch.restart_counts.contains_key(&legacy_key("echo-safety"))); assert!(!orch .restart_last_failure_unix_secs - .contains_key("echo-safety")); + .contains_key(&legacy_key("echo-safety"))); } #[tokio::test] @@ -4016,14 +4974,17 @@ task = "test" // Exit code 0 should never increment restart_count assert_eq!( - orch.restart_counts.get("echo-safety").copied().unwrap_or(0), + orch.restart_counts + .get(&legacy_key("echo-safety")) + .copied() + .unwrap_or(0), 0, "successful exits (code 0) must not increment restart_count" ); // Agent should still be eligible for restart orch.restart_cooldowns.insert( - "echo-safety".to_string(), + legacy_key("echo-safety"), Instant::now() - Duration::from_secs(999), ); orch.restart_pending_safety_agents().await; @@ -4084,7 +5045,10 @@ task = "test" ); // echo exits with code 0, so restart_count stays at 0 assert_eq!( - orch.restart_counts.get("echo-safety").copied().unwrap_or(0), + orch.restart_counts + .get(&legacy_key("echo-safety")) + .copied() + .unwrap_or(0), 0, "successful exit should not increment restart count" ); @@ -4124,6 +5088,8 @@ task = "test" pre_check: None, gitea_issue: None, + + project: None, }]; // Set up persona data dir with a test persona @@ -4209,6 +5175,8 @@ sfia_skills = [{ code = "TEST", name = "Testing", level = 4, description = "Desi pre_check: None, gitea_issue: None, + + project: None, }]; // No persona_data_dir, so registry will be empty @@ -4373,6 +5341,7 @@ sfia_skills = [{ code = "TEST", name = "Testing", level = 4, description = "Desi max_cpu_seconds: Some(1), // 1 second timeout pre_check: None, gitea_issue: None, + project: None, }]; let mut orch = AgentOrchestrator::new(config).unwrap(); let def = orch.config.agents[0].clone(); @@ -4420,6 +5389,7 @@ sfia_skills = [{ code = "TEST", name = "Testing", level = 4, description = "Desi // Add a test flow with a schedule config.flows = vec![FlowDefinition { name: "test-flow".to_string(), + project: "test".to_string(), schedule: Some("0 2 * * *".to_string()), // 2 AM daily repo_path: "/tmp/test-repo".to_string(), base_branch: "main".to_string(), @@ -4508,4 +5478,80 @@ sfia_skills = [{ code = "TEST", name = "Testing", level = 4, description = "Desi result ); } + + /// An agent whose monthly budget is exhausted must skip spawn entirely. + /// `CostTracker::check()` returning `Exhausted` short-circuits + /// dispatch before pre-check or routing runs. + #[tokio::test] + async fn test_spawn_agent_skips_when_budget_exhausted() { + let mut config = test_config_fast_lifecycle(); + config.agents = vec![AgentDefinition { + name: "broke-agent".to_string(), + layer: AgentLayer::Safety, + cli_tool: "echo".to_string(), + task: "should not run".to_string(), + model: None, + schedule: None, + capabilities: vec![], + max_memory_bytes: None, + // $1 monthly budget. + budget_monthly_cents: Some(100), + provider: None, + persona: None, + terraphim_role: None, + skill_chain: vec![], + sfia_skills: vec![], + fallback_provider: None, + fallback_model: None, + grace_period_secs: None, + max_cpu_seconds: None, + pre_check: None, + gitea_issue: None, + project: None, + }]; + + let mut orch = AgentOrchestrator::new(config).unwrap(); + + // Blow through the budget before attempting to spawn. + let verdict = orch.cost_tracker.record_cost("broke-agent", 2.00); + assert!( + verdict.should_pause(), + "budget must be exhausted: {verdict}" + ); + + let def = orch.config.agents[0].clone(); + let result = orch.spawn_agent(&def).await; + + // Spawn should succeed-with-no-op rather than error. + assert!(result.is_ok(), "spawn returned error: {:?}", result); + // Agent must NOT have been added to active_agents. + assert!( + !orch.active_agents.contains_key("broke-agent"), + "exhausted agent should not have been spawned" + ); + } + + /// An agent with no budget cap (subscription) must spawn normally + /// even after recording cost -- `record_cost` returns `Uncapped`. + #[tokio::test] + async fn test_spawn_agent_runs_when_budget_uncapped() { + let mut config = test_config_fast_lifecycle(); + // Ensure the only agent is uncapped. + config.agents[0].budget_monthly_cents = None; + config.agents[0].name = "subscription-agent".to_string(); + + let mut orch = AgentOrchestrator::new(config).unwrap(); + + // Even a large recorded spend must not pause an uncapped agent. + let _ = orch + .cost_tracker + .record_cost("subscription-agent", 9_999.00); + let verdict = orch.cost_tracker.check("subscription-agent"); + assert!(!verdict.should_pause(), "uncapped must never pause"); + + let def = orch.config.agents[0].clone(); + let result = orch.spawn_agent(&def).await; + assert!(result.is_ok(), "spawn errored: {:?}", result); + assert!(orch.active_agents.contains_key("subscription-agent")); + } } diff --git a/crates/terraphim_orchestrator/src/mention.rs b/crates/terraphim_orchestrator/src/mention.rs index 4635053e7..930901e6d 100644 --- a/crates/terraphim_orchestrator/src/mention.rs +++ b/crates/terraphim_orchestrator/src/mention.rs @@ -13,9 +13,17 @@ use std::collections::HashMap; use std::sync::LazyLock; use terraphim_tracker::IssueComment; -/// Regex for @adf:name mentions. -static MENTION_RE: LazyLock = - LazyLock::new(|| Regex::new(r"@adf:([a-z][a-z0-9-]{1,39})\b").unwrap()); +pub(crate) use crate::dispatcher::LEGACY_PROJECT_ID; + +/// Regex for `@adf:[project/]name` mentions. +/// +/// Captures an optional lowercase project prefix and a mandatory agent name. +/// Unqualified mentions (`@adf:developer`) keep the `project` capture as `None`. +/// Qualified mentions (`@adf:odilo/developer`) populate both. +static MENTION_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"@adf:(?:(?P[a-z][a-z0-9-]{1,39})/)?(?P[a-z][a-z0-9-]{1,39})\b") + .unwrap() +}); /// How a mention was resolved. #[derive(Debug, Clone, PartialEq)] @@ -24,6 +32,32 @@ pub enum MentionResolution { PersonaName { persona: String }, } +/// Parsed tokens of a single `@adf:[project/]name` mention. +#[derive(Debug, Clone, PartialEq)] +pub struct MentionTokens { + pub project: Option, + pub agent: String, +} + +/// Parse all `@adf:[project/]name` mentions in `text`, returning their +/// project prefix (if any) and bare agent name in order of appearance. +/// +/// Unlike [`parse_mentions`] this is a pure syntactic pass — no lookup +/// against known agents or personas. Useful for tests and for the +/// multi-project poller which wants the raw tokens before resolution. +pub fn parse_mention_tokens(text: &str) -> Vec { + MENTION_RE + .captures_iter(text) + .map(|caps| MentionTokens { + project: caps.name("project").map(|m| m.as_str().to_string()), + agent: caps + .name("agent") + .map(|m| m.as_str().to_string()) + .unwrap_or_default(), + }) + .collect() +} + /// A detected and resolved mention. #[derive(Debug, Clone)] pub struct DetectedMention { @@ -35,6 +69,14 @@ pub struct DetectedMention { pub comment_body: String, pub mentioner: String, pub timestamp: String, + /// Project id the mention was detected in. + /// + /// Set to [`LEGACY_PROJECT_ID`] for legacy single-project mode or to the + /// id of the project whose repo the enclosing comment was polled from. + /// A qualified `@adf:/` mention does not override this -- the + /// detected project lives in [`MentionTokens`] from [`parse_mention_tokens`] + /// and is consumed by [`resolve_mention`]. + pub project_id: String, } // --------------------------------------------------------------------------- @@ -43,7 +85,9 @@ pub struct DetectedMention { /// Persistent cursor for mention polling. /// -/// Stored via `terraphim_persistence` as JSON at key `adf/mention_cursor`. +/// Stored via `terraphim_persistence` as JSON at key +/// `adf/mention_cursor/` (per-project) or +/// `adf/mention_cursor/__global__` for legacy single-project mode. /// The cursor tracks the `created_at` timestamp of the last processed comment, /// ensuring we never replay historical mentions on restart. /// @@ -110,45 +154,70 @@ impl MentionCursor { Some(op.clone()) } - pub async fn load_or_now() -> Self { - let key = "adf/mention_cursor"; + /// Persistence key for a project's cursor. + /// + /// Multi-project installations use one cursor per project id; legacy + /// single-project installations pass [`LEGACY_PROJECT_ID`]. + fn cursor_key(project_id: &str) -> String { + format!("adf/mention_cursor/{}", project_id) + } + + pub async fn load_or_now(project_id: &str) -> Self { + let key = Self::cursor_key(project_id); if let Some(op) = Self::sqlite_op().await { - if let Ok(bs) = op.read(key).await { + if let Ok(bs) = op.read(&key).await { if let Ok(cursor) = serde_json::from_slice::(&bs.to_vec()) { tracing::info!( + project = project_id, last_seen_at = %cursor.last_seen_at, "loaded MentionCursor from persistence" ); return cursor; } - tracing::warn!("failed to deserialize MentionCursor, starting fresh"); + tracing::warn!( + project = project_id, + "failed to deserialize MentionCursor, starting fresh" + ); } else { - tracing::info!("no persisted MentionCursor found, starting fresh"); + tracing::info!( + project = project_id, + "no persisted MentionCursor found, starting fresh" + ); } } else { - tracing::warn!("DeviceStorage sqlite not available, using in-memory cursor"); + tracing::warn!( + project = project_id, + "DeviceStorage sqlite not available, using in-memory cursor" + ); } Self::now() } - /// Save to persistence. - pub async fn save(&self) { - let key = "adf/mention_cursor"; + /// Save to persistence under the given project's cursor key. + pub async fn save(&self, project_id: &str) { + let key = Self::cursor_key(project_id); if let Some(op) = Self::sqlite_op().await { if let Ok(json) = serde_json::to_string(self) { - if let Err(e) = op.write(key, json).await { - tracing::warn!(?e, "failed to save MentionCursor"); + if let Err(e) = op.write(&key, json).await { + tracing::warn!(project = project_id, ?e, "failed to save MentionCursor"); } else { - tracing::debug!(last_seen_at = %self.last_seen_at, "saved MentionCursor"); + tracing::debug!( + project = project_id, + last_seen_at = %self.last_seen_at, + "saved MentionCursor" + ); } } else { - tracing::warn!("failed to serialize MentionCursor"); + tracing::warn!(project = project_id, "failed to serialize MentionCursor"); } } else { - tracing::warn!("DeviceStorage sqlite not available, cursor not persisted"); + tracing::warn!( + project = project_id, + "DeviceStorage sqlite not available, cursor not persisted" + ); } } @@ -179,12 +248,101 @@ impl Default for MentionCursor { } } -/// Resolve a raw mention to an agent name. +/// One-shot migration of the legacy top-level `adf/mention_cursor` key +/// to per-project keys `adf/mention_cursor/`. +/// +/// Behaviour: +/// +/// - If the legacy key does not exist (or storage is unavailable), the call +/// is a no-op. +/// - If it does exist, the cursor is copied to every project id in +/// `projects` **and** to [`LEGACY_PROJECT_ID`]. A target key is only +/// written when it does not already exist, so repeated invocations +/// never clobber a cursor the poller has already advanced. +/// - After copying, the legacy key is deleted so subsequent restarts +/// skip this path entirely. +/// +/// `projects` is the current orchestrator config's `projects` list. +/// An empty list means legacy single-project mode; the legacy cursor is +/// then simply renamed to the `__global__` key. +/// +/// Logged but non-fatal on any storage error -- the poller will create +/// fresh per-project cursors on first use if migration fails. +pub async fn migrate_legacy_mention_cursor(projects: &[crate::config::Project]) { + let legacy_key = "adf/mention_cursor"; + + let Some(op) = MentionCursor::sqlite_op().await else { + tracing::debug!("mention cursor migration skipped: no sqlite operator"); + return; + }; + + let legacy_bytes = match op.read(legacy_key).await { + Ok(bs) => bs, + Err(_) => { + tracing::debug!("no legacy mention cursor at `adf/mention_cursor`, nothing to migrate"); + return; + } + }; + + let Ok(cursor) = serde_json::from_slice::(&legacy_bytes.to_vec()) else { + tracing::warn!( + "legacy mention cursor is unparsable; deleting it so per-project keys start clean" + ); + let _ = op.delete(legacy_key).await; + return; + }; + + let Ok(json) = serde_json::to_string(&cursor) else { + tracing::warn!("failed to serialize legacy mention cursor during migration"); + return; + }; + + let mut targets: Vec = projects.iter().map(|p| p.id.clone()).collect(); + targets.push(LEGACY_PROJECT_ID.to_string()); + + let mut written = 0usize; + for pid in &targets { + let key = MentionCursor::cursor_key(pid); + match op.stat(&key).await { + Ok(_) => { + tracing::debug!( + project = pid.as_str(), + "skipping legacy-cursor migration: per-project cursor already present" + ); + } + Err(_) => { + if let Err(e) = op.write(&key, json.clone()).await { + tracing::warn!( + project = pid.as_str(), + ?e, + "failed to write migrated MentionCursor" + ); + } else { + written += 1; + } + } + } + } + + match op.delete(legacy_key).await { + Ok(()) => tracing::info!( + migrated_to = written, + last_seen_at = %cursor.last_seen_at, + "migrated legacy mention cursor to per-project keys" + ), + Err(e) => tracing::warn!(?e, "failed to delete legacy mention cursor after migration"), + } +} + +/// Resolve a raw mention to an agent name via persona lookup. /// /// 1. If raw matches an agent name exactly -> AgentName /// 2. If raw matches a persona name -> PersonaName (pick best-fit agent) /// 3. No match -> None -pub fn resolve_mention( +/// +/// This is the legacy single-project resolver used by the compound-review +/// persona dispatch path. Multi-project resolution lives in [`resolve_mention`]. +pub fn resolve_persona_mention( raw: &str, agents: &[AgentDefinition], personas: &PersonaRegistry, @@ -248,33 +406,121 @@ pub fn resolve_mention( None } +/// Resolve a `@adf:[project/]name` mention to a concrete [`AgentDefinition`] +/// using the poller's project hint and optional qualified prefix. +/// +/// Resolution rules (in order): +/// +/// 1. If `detected_project` is `Some("p")` — an explicit `@adf:p/name` — +/// find the unique agent whose `name == agent_name` **and** `project == Some("p")`. +/// Mismatch between `detected_project` and `hinted_project` is permitted: +/// a user in repo A may address an agent defined against project B, as long +/// as that agent exists. If zero or more than one agent matches, return `None`. +/// +/// 2. If `detected_project` is `None` — an unqualified `@adf:name`: +/// +/// - If `hinted_project == `[`LEGACY_PROJECT_ID`] (single-project legacy mode), +/// match on `name == agent_name` only, ignoring the agent's `project` field. +/// +/// - Otherwise, prefer an agent defined for `hinted_project` +/// (`name == agent_name` and `project == Some(hinted_project)`). +/// +/// - Fallback: accept a project-less agent (`project == None`) with a matching name. +/// Cross-project defaulting (matching an agent whose project differs from +/// the hint) is never allowed -- that would let an unqualified mention +/// silently spawn an agent from another repo. +/// +/// Returns `None` if no agent satisfies these rules or if multiple agents do. +/// +/// The caller is expected to have obtained `detected_project` from +/// [`parse_mention_tokens`] or [`MENTION_RE`] and `hinted_project` from the +/// poller's current iteration over `config.projects` (or [`LEGACY_PROJECT_ID`] +/// for the legacy top-level gitea path). +pub fn resolve_mention( + detected_project: Option<&str>, + hinted_project: &str, + agent_name: &str, + agents: &[AgentDefinition], +) -> Option { + if let Some(proj) = detected_project { + // Qualified form: `@adf:/` — exact (name, project) match. + let matches: Vec<&AgentDefinition> = agents + .iter() + .filter(|a| a.name == agent_name && a.project.as_deref() == Some(proj)) + .collect(); + return match matches.len() { + 1 => Some(matches[0].clone()), + _ => None, + }; + } + + // Unqualified form: `@adf:`. + if hinted_project == LEGACY_PROJECT_ID { + // Legacy single-project mode: ignore the agent's project field. + let matches: Vec<&AgentDefinition> = + agents.iter().filter(|a| a.name == agent_name).collect(); + return match matches.len() { + 1 => Some(matches[0].clone()), + _ => None, + }; + } + + // Multi-project mode: prefer an agent bound to the hinted project. + let hinted: Vec<&AgentDefinition> = agents + .iter() + .filter(|a| a.name == agent_name && a.project.as_deref() == Some(hinted_project)) + .collect(); + if hinted.len() == 1 { + return Some(hinted[0].clone()); + } + if hinted.len() > 1 { + return None; + } + + // Fallback: project-less agent with matching name. + let unbound: Vec<&AgentDefinition> = agents + .iter() + .filter(|a| a.name == agent_name && a.project.is_none()) + .collect(); + match unbound.len() { + 1 => Some(unbound[0].clone()), + _ => None, + } +} + /// Parse and resolve all @adf:name mentions from a comment. +/// +/// `hinted_project` is the id of the project whose repo the comment was +/// polled from, or [`LEGACY_PROJECT_ID`] in single-project mode. It is +/// stamped on every produced [`DetectedMention`] for downstream dispatch. pub fn parse_mentions( comment: &IssueComment, issue_number: u64, agents: &[AgentDefinition], personas: &PersonaRegistry, + hinted_project: &str, ) -> Vec { let mut mentions = Vec::new(); for cap in MENTION_RE.captures_iter(&comment.body) { - let raw = &cap[1]; + let raw_agent = cap.name("agent").map(|m| m.as_str()).unwrap_or_default(); if let Some((agent_name, resolution)) = - resolve_mention(raw, agents, personas, &comment.body) + resolve_persona_mention(raw_agent, agents, personas, &comment.body) { mentions.push(DetectedMention { issue_number, comment_id: comment.id, - raw_mention: raw.to_string(), + raw_mention: raw_agent.to_string(), agent_name, resolution, comment_body: comment.body.clone(), mentioner: comment.user.login.clone(), timestamp: comment.created_at.clone(), + project_id: hinted_project.to_string(), }); } else { tracing::warn!( - raw_mention = raw, + raw_mention = raw_agent, issue = issue_number, "unresolved @adf mention" ); @@ -350,6 +596,7 @@ mod tests { max_cpu_seconds: None, pre_check: None, gitea_issue: None, + project: None, } } @@ -430,11 +677,12 @@ mod tests { let agents = test_agents(); let personas = test_personas(); let comment = make_comment(1, "Please @adf:security-sentinel review this code", "alice"); - let mentions = parse_mentions(&comment, 42, &agents, &personas); + let mentions = parse_mentions(&comment, 42, &agents, &personas, LEGACY_PROJECT_ID); assert_eq!(mentions.len(), 1); assert_eq!(mentions[0].agent_name, "security-sentinel"); assert_eq!(mentions[0].resolution, MentionResolution::AgentName); assert_eq!(mentions[0].raw_mention, "security-sentinel"); + assert_eq!(mentions[0].project_id, LEGACY_PROJECT_ID); } #[test] @@ -444,7 +692,7 @@ mod tests { // "vigil" persona resolves to an agent. With "security" in context, // should prefer security-sentinel over compliance-watchdog. let comment = make_comment(2, "@adf:vigil check for security vulnerabilities", "alice"); - let mentions = parse_mentions(&comment, 42, &agents, &personas); + let mentions = parse_mentions(&comment, 42, &agents, &personas, LEGACY_PROJECT_ID); assert_eq!(mentions.len(), 1); assert_eq!(mentions[0].agent_name, "security-sentinel"); assert!(matches!( @@ -458,7 +706,7 @@ mod tests { let agents = test_agents(); let personas = test_personas(); let comment = make_comment(3, "@adf:vigil and @adf:carthos please review", "bob"); - let mentions = parse_mentions(&comment, 42, &agents, &personas); + let mentions = parse_mentions(&comment, 42, &agents, &personas, LEGACY_PROJECT_ID); assert_eq!(mentions.len(), 2); } @@ -467,7 +715,7 @@ mod tests { let agents = test_agents(); let personas = test_personas(); let comment = make_comment(4, "No mentions here", "alice"); - let mentions = parse_mentions(&comment, 42, &agents, &personas); + let mentions = parse_mentions(&comment, 42, &agents, &personas, LEGACY_PROJECT_ID); assert!(mentions.is_empty()); } @@ -476,16 +724,26 @@ mod tests { let agents = test_agents(); let personas = test_personas(); let comment = make_comment(5, "@alice please review", "bob"); - let mentions = parse_mentions(&comment, 42, &agents, &personas); + let mentions = parse_mentions(&comment, 42, &agents, &personas, LEGACY_PROJECT_ID); assert!(mentions.is_empty()); } + #[test] + fn test_parse_stamps_hinted_project_id() { + let agents = test_agents(); + let personas = test_personas(); + let comment = make_comment(6, "Please @adf:security-sentinel look at this", "alice"); + let mentions = parse_mentions(&comment, 42, &agents, &personas, "odilo"); + assert_eq!(mentions.len(), 1); + assert_eq!(mentions[0].project_id, "odilo"); + } + #[test] fn test_resolve_persona_single_agent() { let agents = test_agents(); let personas = test_personas(); // Lux has only one agent: product-development - let result = resolve_mention("lux", &agents, &personas, "some context"); + let result = resolve_persona_mention("lux", &agents, &personas, "some context"); assert!(result.is_some()); let (name, res) = result.unwrap(); assert_eq!(name, "product-development"); @@ -498,7 +756,8 @@ mod tests { let personas = test_personas(); // Vigil shared by security-sentinel and compliance-watchdog // Context mentions "license" -> should pick compliance-watchdog - let result = resolve_mention("vigil", &agents, &personas, "check license compliance"); + let result = + resolve_persona_mention("vigil", &agents, &personas, "check license compliance"); assert!(result.is_some()); let (name, _) = result.unwrap(); assert_eq!(name, "compliance-watchdog"); @@ -508,7 +767,7 @@ mod tests { fn test_resolve_unknown_name() { let agents = test_agents(); let personas = test_personas(); - let result = resolve_mention("nonexistent", &agents, &personas, "context"); + let result = resolve_persona_mention("nonexistent", &agents, &personas, "context"); assert!(result.is_none()); } diff --git a/crates/terraphim_orchestrator/src/mode/issue.rs b/crates/terraphim_orchestrator/src/mode/issue.rs index fd1c2f800..c9d643c1e 100644 --- a/crates/terraphim_orchestrator/src/mode/issue.rs +++ b/crates/terraphim_orchestrator/src/mode/issue.rs @@ -21,6 +21,8 @@ pub struct IssueMode { concurrency: ConcurrencyController, /// Set of running issue IDs to prevent duplicates. running: std::collections::HashSet, + /// Project id that owns the tracker driving this mode. + project: String, } impl IssueMode { @@ -29,6 +31,21 @@ impl IssueMode { config: WorkflowConfig, tracker: Box, concurrency: ConcurrencyController, + ) -> Self { + Self::with_project( + config, + tracker, + concurrency, + crate::dispatcher::LEGACY_PROJECT_ID.to_string(), + ) + } + + /// Create a new issue mode controller bound to a specific project id. + pub fn with_project( + config: WorkflowConfig, + tracker: Box, + concurrency: ConcurrencyController, + project: String, ) -> Self { let pagerank = if config.tracker.use_robot_api { Some(PagerankClient::new( @@ -46,6 +63,7 @@ impl IssueMode { dispatcher: Dispatcher::new(), concurrency, running: std::collections::HashSet::new(), + project, } } @@ -144,8 +162,8 @@ impl IssueMode { continue; } - // Try to acquire concurrency slot - match self.concurrency.acquire_issue_driven().await { + // Try to acquire concurrency slot for this project + match self.concurrency.acquire_issue_driven(&self.project).await { Some(permit) => { // Create dispatch task let task = DispatchTask::IssueDriven { @@ -153,6 +171,7 @@ impl IssueMode { title: issue.title.clone(), priority: issue.priority, pagerank_score: issue.pagerank_score, + project: self.project.clone(), }; self.dispatcher.enqueue(task); diff --git a/crates/terraphim_orchestrator/src/mode/time.rs b/crates/terraphim_orchestrator/src/mode/time.rs index 6b56d0c9f..daddf9b00 100644 --- a/crates/terraphim_orchestrator/src/mode/time.rs +++ b/crates/terraphim_orchestrator/src/mode/time.rs @@ -38,10 +38,15 @@ impl TimeMode { /// Start a Safety agent immediately. pub async fn start_safety_agent(&mut self, agent: AgentDefinition) -> Result<(), String> { // Safety agents bypass concurrency limits (they're always on) + let project = agent + .project + .clone() + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()); let task = DispatchTask::TimeDriven { name: agent.name.clone(), task: agent.task.clone(), layer: agent.layer, + project, }; self.dispatcher.enqueue(task); @@ -101,13 +106,18 @@ impl TimeMode { /// Handle a spawn event. async fn handle_spawn(&mut self, agent: AgentDefinition) -> Result<(), String> { - // Try to acquire a time-driven slot - match self.concurrency.acquire_time_driven().await { + let project = agent + .project + .clone() + .unwrap_or_else(|| crate::dispatcher::LEGACY_PROJECT_ID.to_string()); + // Try to acquire a time-driven slot for this project + match self.concurrency.acquire_time_driven(&project).await { Some(permit) => { let task = DispatchTask::TimeDriven { name: agent.name.clone(), task: agent.task.clone(), layer: agent.layer, + project, }; self.dispatcher.enqueue(task); @@ -179,6 +189,8 @@ mod tests { pre_check: None, gitea_issue: None, + + project: None, } } diff --git a/crates/terraphim_orchestrator/src/output_poster.rs b/crates/terraphim_orchestrator/src/output_poster.rs index 9046f5fbb..62ce80c54 100644 --- a/crates/terraphim_orchestrator/src/output_poster.rs +++ b/crates/terraphim_orchestrator/src/output_poster.rs @@ -1,136 +1,166 @@ //! Posts agent output to Gitea issues after agent exit. +//! +//! Supports multi-project configurations where each project owns its own +//! `owner`/`repo`/`token` triple. The legacy single-project mode is retained +//! by keying the default tracker on [`crate::dispatcher::LEGACY_PROJECT_ID`]. use std::collections::HashMap; +use std::path::PathBuf; -use terraphim_tracker::gitea::GiteaConfig; +use terraphim_tracker::gitea::{ClaimStrategy, GiteaConfig}; use terraphim_tracker::GiteaTracker; -use crate::config::GiteaOutputConfig; +use crate::config::{GiteaOutputConfig, OrchestratorConfig}; +use crate::dispatcher::LEGACY_PROJECT_ID; + +const ROBOT_PATH: &str = "/home/alex/go/bin/gitea-robot"; + +/// Trackers for a single project (root token + any per-agent token overrides). +struct ProjectTrackers { + /// Default tracker using the project-level token from config. + default_tracker: GiteaTracker, + /// Per-agent trackers keyed by agent name (scoped to this project). + agent_trackers: HashMap, +} /// Posts collected agent output to a Gitea issue comment. /// -/// Supports per-agent Gitea tokens so each agent posts under its own user. -/// Falls back to the default (root) token when no agent-specific token exists. +/// Supports per-project Gitea targets (owner/repo) and per-agent tokens so +/// each agent posts under its own user within its owning project. Falls back +/// to the legacy (top-level) Gitea config when no project routing is defined. pub struct OutputPoster { - /// Default tracker using the root token from config. - default_tracker: GiteaTracker, - /// Per-agent trackers keyed by agent name. - agent_trackers: HashMap, - /// Base config retained for diagnostics. + /// Trackers keyed by project id. + projects: HashMap, + /// Fallback project id used when the caller doesn't know which project to + /// post against (e.g. compound review posts on legacy configs). In + /// multi-project mode this stays `None` and callers must pass a project id. + fallback_project: Option, + /// Base URL retained for diagnostics. #[allow(dead_code)] base_url: String, } impl OutputPoster { - /// Create a new OutputPoster from Gitea output configuration. + /// Build a single-project poster from a bare Gitea output config. /// - /// If `agent_tokens_path` is set, loads the JSON file as a - /// `HashMap` mapping agent names to Gitea API tokens. + /// Used by tests and by the legacy (single-project) code path — the + /// resulting poster stores everything under + /// [`crate::dispatcher::LEGACY_PROJECT_ID`] and uses that as the fallback + /// project id. pub fn new(config: &GiteaOutputConfig) -> Self { - let default_gitea_config = GiteaConfig { - base_url: config.base_url.clone(), - token: config.token.clone(), - owner: config.owner.clone(), - repo: config.repo.clone(), - active_states: vec!["open".to_string()], - terminal_states: vec!["closed".to_string()], - use_robot_api: false, - robot_path: std::path::PathBuf::from("/home/alex/go/bin/gitea-robot"), - claim_strategy: terraphim_tracker::gitea::ClaimStrategy::PreferRobot, - }; - let default_tracker = - GiteaTracker::new(default_gitea_config).expect("Failed to create default GiteaTracker"); - - // Load per-agent tokens if path is configured - let agent_trackers = match &config.agent_tokens_path { - Some(path) => match std::fs::read_to_string(path) { - Ok(contents) => match serde_json::from_str::>(&contents) { - Ok(tokens) => { - tracing::info!( - count = tokens.len(), - path = %path.display(), - "loaded per-agent Gitea tokens" - ); - let mut trackers = HashMap::with_capacity(tokens.len()); - for (agent_name, token) in tokens { - let agent_config = GiteaConfig { - base_url: config.base_url.clone(), - token, - owner: config.owner.clone(), - repo: config.repo.clone(), - active_states: vec!["open".to_string()], - terminal_states: vec!["closed".to_string()], - use_robot_api: false, - robot_path: std::path::PathBuf::from( - "/home/alex/go/bin/gitea-robot", - ), - claim_strategy: - terraphim_tracker::gitea::ClaimStrategy::PreferRobot, - }; - match GiteaTracker::new(agent_config) { - Ok(tracker) => { - trackers.insert(agent_name, tracker); - } - Err(e) => { - tracing::warn!( - agent = %agent_name, - error = %e, - "failed to create agent tracker, will use default" - ); - } - } - } - trackers - } - Err(e) => { - tracing::warn!( - path = %path.display(), - error = %e, - "failed to parse agent tokens JSON, all agents will use default token" - ); - HashMap::new() - } - }, - Err(e) => { - tracing::warn!( - path = %path.display(), - error = %e, - "failed to read agent tokens file, all agents will use default token" - ); - HashMap::new() - } - }, - None => HashMap::new(), - }; + let trackers = build_project_trackers(config); + + let mut projects = HashMap::new(); + projects.insert(LEGACY_PROJECT_ID.to_string(), trackers); Self { - default_tracker, - agent_trackers, + projects, + fallback_project: Some(LEGACY_PROJECT_ID.to_string()), base_url: config.base_url.clone(), } } - /// Get the tracker for a specific agent, falling back to the default. - pub fn tracker_for(&self, agent_name: &str) -> &GiteaTracker { - self.agent_trackers - .get(agent_name) - .unwrap_or(&self.default_tracker) + /// Build a poster from a full orchestrator config, wiring one entry per + /// project as well as a legacy fallback from the top-level `config.gitea`. + /// + /// When `config.projects` is empty this behaves exactly like + /// [`OutputPoster::new`] against the top-level gitea config. + pub fn from_orchestrator_config(config: &OrchestratorConfig) -> Option { + let mut projects: HashMap = HashMap::new(); + let mut base_url = String::new(); + + // Per-project trackers. + for project in &config.projects { + let Some(gitea_cfg) = project.gitea.as_ref().or(config.gitea.as_ref()) else { + continue; + }; + base_url = gitea_cfg.base_url.clone(); + projects.insert(project.id.clone(), build_project_trackers(gitea_cfg)); + } + + // Legacy fallback from top-level gitea block. + let fallback_project = if let Some(ref top) = config.gitea { + base_url = top.base_url.clone(); + projects.insert(LEGACY_PROJECT_ID.to_string(), build_project_trackers(top)); + Some(LEGACY_PROJECT_ID.to_string()) + } else { + // No top-level config: pick an arbitrary project id as fallback so + // callers that don't know their project (e.g. compound review on a + // multi-project fleet) still have somewhere to post. + projects.keys().next().cloned() + }; + + if projects.is_empty() { + return None; + } + + Some(Self { + projects, + fallback_project, + base_url, + }) + } + + /// Return the tracker to use for `(project, agent)`. + /// + /// Resolution order: + /// 1. Per-agent tracker within the requested project. + /// 2. Project default tracker. + /// 3. Fallback project's default tracker (legacy behaviour). + pub fn tracker_for(&self, project: &str, agent_name: &str) -> Option<&GiteaTracker> { + if let Some(p) = self.projects.get(project) { + if let Some(agent_tracker) = p.agent_trackers.get(agent_name) { + return Some(agent_tracker); + } + return Some(&p.default_tracker); + } + let fallback = self.fallback_project.as_deref()?; + let p = self.projects.get(fallback)?; + Some( + p.agent_trackers + .get(agent_name) + .unwrap_or(&p.default_tracker), + ) + } + + /// Return the default (root-token) tracker for the requested project, + /// falling back to the fallback project. + fn default_tracker_for(&self, project: &str) -> Option<&GiteaTracker> { + if let Some(p) = self.projects.get(project) { + return Some(&p.default_tracker); + } + let fallback = self.fallback_project.as_deref()?; + self.projects.get(fallback).map(|p| &p.default_tracker) } - /// Post agent output as a comment on the given Gitea issue. + /// Whether the tracker for `(project, agent)` is using an agent-specific + /// token (rather than the project default). + fn has_own_token(&self, project: &str, agent_name: &str) -> bool { + self.projects + .get(project) + .is_some_and(|p| p.agent_trackers.contains_key(agent_name)) + } + + /// Post agent output as a comment on a Gitea issue in the given project. /// /// Uses the agent's own Gitea token if configured, otherwise falls back - /// to the default token. Truncates output to 60000 bytes to stay within + /// to the project default. Truncates output to 60000 bytes to stay within /// Gitea's comment size limit. - pub async fn post_agent_output( + pub async fn post_agent_output_for_project( &self, + project: &str, agent_name: &str, issue_number: u64, output_lines: &[String], exit_code: Option, ) -> Result<(), String> { if output_lines.is_empty() { - tracing::debug!(agent = %agent_name, issue = issue_number, "no output to post"); + tracing::debug!( + project = project, + agent = %agent_name, + issue = issue_number, + "no output to post" + ); return Ok(()); } @@ -147,7 +177,6 @@ impl OutputPoster { ); let joined = output_lines.join("\n"); - // Truncate to stay within Gitea limits (~65535 bytes) let max_output = 60000; if joined.len() > max_output { body.push_str(&joined[..max_output]); @@ -157,35 +186,81 @@ impl OutputPoster { } body.push_str("\n```\n\n"); - let tracker = self.tracker_for(agent_name); + let Some(tracker) = self.tracker_for(project, agent_name) else { + let msg = format!( + "no Gitea tracker configured for project {} and no fallback available", + project + ); + tracing::error!("{}", msg); + return Err(msg); + }; + match tracker.post_comment(issue_number, &body).await { Ok(comment) => { - let has_own_token = self.agent_trackers.contains_key(agent_name); tracing::info!( + project = project, agent = %agent_name, issue = issue_number, comment_id = comment.id, - own_token = has_own_token, + own_token = self.has_own_token(project, agent_name), "posted agent output to Gitea" ); Ok(()) } Err(e) => { - let msg = format!("failed to post output for {}: {}", agent_name, e); + let msg = format!( + "failed to post output for {} in project {}: {}", + agent_name, project, e + ); tracing::error!("{}", msg); Err(msg) } } } - /// Post raw markdown as a comment on the given Gitea issue. - /// - /// Uses the default (root) token. For agent-specific posting, use - /// `post_agent_output` instead. - pub async fn post_raw(&self, issue_number: u64, body: &str) -> Result<(), String> { - match self.default_tracker.post_comment(issue_number, body).await { + /// Legacy-compatible post using the fallback project. + pub async fn post_agent_output( + &self, + agent_name: &str, + issue_number: u64, + output_lines: &[String], + exit_code: Option, + ) -> Result<(), String> { + let project = self.fallback_project.clone().unwrap_or_else(|| { + tracing::warn!( + agent = %agent_name, + "no fallback project for legacy post_agent_output; defaulting to {}", + LEGACY_PROJECT_ID + ); + LEGACY_PROJECT_ID.to_string() + }); + self.post_agent_output_for_project( + &project, + agent_name, + issue_number, + output_lines, + exit_code, + ) + .await + } + + /// Post raw markdown as a comment on a Gitea issue using the project + /// default (root-token) tracker. + pub async fn post_raw_for_project( + &self, + project: &str, + issue_number: u64, + body: &str, + ) -> Result<(), String> { + let Some(tracker) = self.default_tracker_for(project) else { + let msg = format!("no Gitea tracker configured for project {}", project); + tracing::error!("{}", msg); + return Err(msg); + }; + match tracker.post_comment(issue_number, body).await { Ok(comment) => { tracing::info!( + project = project, issue = issue_number, comment_id = comment.id, "posted raw comment to Gitea" @@ -193,37 +268,58 @@ impl OutputPoster { Ok(()) } Err(e) => { - let msg = format!("failed to post comment to issue {}: {}", issue_number, e); + let msg = format!( + "failed to post comment to issue {} in project {}: {}", + issue_number, project, e + ); tracing::error!("{}", msg); Err(msg) } } } - /// Post raw markdown as a specific agent (using agent's own token if available). - pub async fn post_raw_as_agent( + /// Legacy-compatible raw post using the fallback project. + pub async fn post_raw(&self, issue_number: u64, body: &str) -> Result<(), String> { + let project = self + .fallback_project + .clone() + .unwrap_or_else(|| LEGACY_PROJECT_ID.to_string()); + self.post_raw_for_project(&project, issue_number, body) + .await + } + + /// Post raw markdown as a specific agent within a project. + pub async fn post_raw_as_agent_for_project( &self, + project: &str, agent_name: &str, issue_number: u64, body: &str, ) -> Result<(), String> { - let tracker = self.tracker_for(agent_name); + let Some(tracker) = self.tracker_for(project, agent_name) else { + let msg = format!( + "no Gitea tracker configured for project {} and no fallback available", + project + ); + tracing::error!("{}", msg); + return Err(msg); + }; match tracker.post_comment(issue_number, body).await { Ok(comment) => { - let has_own_token = self.agent_trackers.contains_key(agent_name); tracing::info!( + project = project, agent = %agent_name, issue = issue_number, comment_id = comment.id, - own_token = has_own_token, + own_token = self.has_own_token(project, agent_name), "posted raw comment as agent" ); Ok(()) } Err(e) => { let msg = format!( - "failed to post comment as {} to issue {}: {}", - agent_name, issue_number, e + "failed to post comment as {} to issue {} in project {}: {}", + agent_name, issue_number, project, e ); tracing::error!("{}", msg); Err(msg) @@ -231,8 +327,123 @@ impl OutputPoster { } } - /// Get a reference to the underlying default GiteaTracker. - pub fn tracker(&self) -> &terraphim_tracker::GiteaTracker { - &self.default_tracker + /// Legacy-compatible agent-scoped raw post using the fallback project. + pub async fn post_raw_as_agent( + &self, + agent_name: &str, + issue_number: u64, + body: &str, + ) -> Result<(), String> { + let project = self + .fallback_project + .clone() + .unwrap_or_else(|| LEGACY_PROJECT_ID.to_string()); + self.post_raw_as_agent_for_project(&project, agent_name, issue_number, body) + .await + } + + /// Reference to the underlying default tracker for the fallback project. + /// + /// Retained for backwards compatibility with code that reaches into the + /// poster to drive Gitea directly. In multi-project mode, prefer + /// [`OutputPoster::default_tracker_for_project`]. + pub fn tracker(&self) -> &GiteaTracker { + let fallback = self + .fallback_project + .as_deref() + .expect("OutputPoster constructed without a fallback project"); + &self + .projects + .get(fallback) + .expect("fallback project always populated") + .default_tracker + } + + /// Reference to the default tracker for a specific project. + pub fn default_tracker_for_project(&self, project: &str) -> Option<&GiteaTracker> { + self.default_tracker_for(project) + } +} + +/// Build a `ProjectTrackers` bundle for one Gitea output config, including +/// agent-specific tokens loaded from `config.agent_tokens_path` if set. +fn build_project_trackers(config: &GiteaOutputConfig) -> ProjectTrackers { + let default_gitea_config = GiteaConfig { + base_url: config.base_url.clone(), + token: config.token.clone(), + owner: config.owner.clone(), + repo: config.repo.clone(), + active_states: vec!["open".to_string()], + terminal_states: vec!["closed".to_string()], + use_robot_api: false, + robot_path: PathBuf::from(ROBOT_PATH), + claim_strategy: ClaimStrategy::PreferRobot, + }; + let default_tracker = + GiteaTracker::new(default_gitea_config).expect("Failed to create default GiteaTracker"); + + let agent_trackers = match &config.agent_tokens_path { + Some(path) => match std::fs::read_to_string(path) { + Ok(contents) => match serde_json::from_str::>(&contents) { + Ok(tokens) => { + tracing::info!( + count = tokens.len(), + path = %path.display(), + owner = %config.owner, + repo = %config.repo, + "loaded per-agent Gitea tokens" + ); + let mut trackers = HashMap::with_capacity(tokens.len()); + for (agent_name, token) in tokens { + let agent_config = GiteaConfig { + base_url: config.base_url.clone(), + token, + owner: config.owner.clone(), + repo: config.repo.clone(), + active_states: vec!["open".to_string()], + terminal_states: vec!["closed".to_string()], + use_robot_api: false, + robot_path: PathBuf::from(ROBOT_PATH), + claim_strategy: ClaimStrategy::PreferRobot, + }; + match GiteaTracker::new(agent_config) { + Ok(tracker) => { + trackers.insert(agent_name, tracker); + } + Err(e) => { + tracing::warn!( + agent = %agent_name, + error = %e, + "failed to create agent tracker, will use project default" + ); + } + } + } + trackers + } + Err(e) => { + tracing::warn!( + path = %path.display(), + error = %e, + "failed to parse agent tokens JSON, all agents will use project default token" + ); + HashMap::new() + } + }, + Err(e) => { + tracing::warn!( + path = %path.display(), + error = %e, + "failed to read agent tokens file, all agents will use project default token" + ); + HashMap::new() + } + }, + None => HashMap::new(), + }; + + ProjectTrackers { + default_tracker, + agent_trackers, } } diff --git a/crates/terraphim_orchestrator/src/project_control.rs b/crates/terraphim_orchestrator/src/project_control.rs new file mode 100644 index 000000000..64cd2484b --- /dev/null +++ b/crates/terraphim_orchestrator/src/project_control.rs @@ -0,0 +1,287 @@ +//! Project-level operational controls: pause flags and circuit breakers. +//! +//! A pause flag is an empty sentinel file at `/` that +//! causes the orchestrator to skip all dispatches for that project until the +//! file is removed. The default directory is `/opt/ai-dark-factory/data/pause` +//! and is configurable via [`crate::config::OrchestratorConfig::pause_dir`]. +//! +//! The project circuit breaker tracks consecutive `project-meta` failures per +//! project. When the threshold is reached, the orchestrator touches the pause +//! flag and opens an `[ADF]` Gitea issue to notify operators. +//! +//! The counter is intentionally narrow: it only advances on `project-meta` +//! exits so that a flaky developer agent or reviewer cannot trip a +//! project-wide pause. + +use std::collections::HashMap; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +use crate::config::AgentDefinition; + +/// Default directory containing per-project pause flag files. +pub const DEFAULT_PAUSE_DIR: &str = "/opt/ai-dark-factory/data/pause"; + +/// Default number of consecutive `project-meta` failures that trips the +/// project-level pause. +pub const DEFAULT_PROJECT_CIRCUIT_BREAKER_THRESHOLD: u32 = 3; + +/// Return `true` when a pause flag file exists for the given project id. +/// +/// Passing `None` always returns `false` (legacy/global agents are never +/// paused by this mechanism). +pub fn is_project_paused(pause_dir: &Path, project_id: Option<&str>) -> bool { + let Some(pid) = project_id else { + return false; + }; + pause_dir.join(pid).exists() +} + +/// Create the pause flag file for a project. Returns the final path on +/// success. Errors include missing parent dir permissions. +pub fn touch_pause_flag(pause_dir: &Path, project_id: &str) -> io::Result { + fs::create_dir_all(pause_dir)?; + let path = pause_dir.join(project_id); + // Create-or-touch. We do not care about content; existence is the signal. + fs::OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(&path)?; + Ok(path) +} + +/// Outcome returned by [`ProjectFailureCounter::record_project_meta_result`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShouldPause { + /// Keep running; the counter has not reached the threshold. + No, + /// The threshold was just reached. The caller must touch the pause flag + /// and open the escalation issue. Subsequent failures will not re-trip + /// until the counter is reset by a success. + Yes, +} + +/// Heuristic check: does this agent definition correspond to a per-project +/// `project-meta` agent? +/// +/// Exact name `project-meta` or any `project-meta-` form qualifies. +/// The suffix form supports multi-instance deployments (e.g. +/// `project-meta-odilo`, `project-meta-digital-twins`). +pub fn is_project_meta_agent(def: &AgentDefinition) -> bool { + def.name == "project-meta" || def.name.starts_with("project-meta-") +} + +/// Consecutive-failure counter scoped to a single project's `project-meta` +/// agent. +#[derive(Debug, Clone)] +pub struct ProjectFailureCounter { + threshold: u32, + counts: HashMap, + tripped: HashMap, +} + +impl ProjectFailureCounter { + /// Create a new counter with the given trip threshold. Zero is treated as + /// one (any failure trips immediately), which exists mainly for tests. + pub fn new(threshold: u32) -> Self { + Self { + threshold: threshold.max(1), + counts: HashMap::new(), + tripped: HashMap::new(), + } + } + + /// Record the outcome of a single `project-meta` run. Success resets the + /// counter (and the tripped flag). Failure increments and returns + /// [`ShouldPause::Yes`] exactly once when the threshold is crossed. + pub fn record_project_meta_result(&mut self, project_id: &str, success: bool) -> ShouldPause { + if success { + self.counts.remove(project_id); + self.tripped.remove(project_id); + return ShouldPause::No; + } + let entry = self.counts.entry(project_id.to_string()).or_insert(0); + *entry += 1; + let already_tripped = self.tripped.get(project_id).copied().unwrap_or(false); + if *entry >= self.threshold && !already_tripped { + self.tripped.insert(project_id.to_string(), true); + ShouldPause::Yes + } else { + ShouldPause::No + } + } + + /// Current failure count for a project (for diagnostics / tests). + pub fn count(&self, project_id: &str) -> u32 { + self.counts.get(project_id).copied().unwrap_or(0) + } + + /// Trip threshold. + pub fn threshold(&self) -> u32 { + self.threshold + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{AgentDefinition, AgentLayer}; + use tempfile::tempdir; + + fn def_with_name(name: &str) -> AgentDefinition { + AgentDefinition { + name: name.to_string(), + layer: AgentLayer::Core, + cli_tool: "/bin/true".to_string(), + task: String::new(), + schedule: None, + model: None, + capabilities: Vec::new(), + max_memory_bytes: None, + budget_monthly_cents: None, + provider: None, + persona: None, + terraphim_role: None, + skill_chain: Vec::new(), + sfia_skills: Vec::new(), + fallback_provider: None, + fallback_model: None, + grace_period_secs: None, + max_cpu_seconds: None, + pre_check: None, + gitea_issue: None, + project: Some("odilo".to_string()), + } + } + + #[test] + fn pause_flag_absent_means_not_paused() { + let dir = tempdir().unwrap(); + assert!(!is_project_paused(dir.path(), Some("odilo"))); + } + + #[test] + fn pause_flag_present_means_paused() { + let dir = tempdir().unwrap(); + touch_pause_flag(dir.path(), "odilo").unwrap(); + assert!(is_project_paused(dir.path(), Some("odilo"))); + } + + #[test] + fn pause_flag_not_shared_across_projects() { + let dir = tempdir().unwrap(); + touch_pause_flag(dir.path(), "odilo").unwrap(); + assert!(is_project_paused(dir.path(), Some("odilo"))); + assert!(!is_project_paused(dir.path(), Some("digital-twins"))); + } + + #[test] + fn pause_flag_ignored_for_legacy_global_agents() { + let dir = tempdir().unwrap(); + touch_pause_flag(dir.path(), "__global__").unwrap(); + assert!(!is_project_paused(dir.path(), None)); + } + + #[test] + fn touching_pause_flag_creates_parent_dir() { + let root = tempdir().unwrap(); + let nested = root.path().join("deep/dir/pause"); + let flag = touch_pause_flag(&nested, "odilo").unwrap(); + assert!(flag.exists()); + } + + #[test] + fn identifies_project_meta_agent_by_name() { + assert!(is_project_meta_agent(&def_with_name("project-meta"))); + assert!(is_project_meta_agent(&def_with_name("project-meta-odilo"))); + assert!(!is_project_meta_agent(&def_with_name("fleet-meta"))); + assert!(!is_project_meta_agent(&def_with_name("meta-coordinator"))); + assert!(!is_project_meta_agent(&def_with_name("developer"))); + } + + #[test] + fn counter_resets_on_success() { + let mut c = ProjectFailureCounter::new(3); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::No + ); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::No + ); + assert_eq!(c.count("odilo"), 2); + assert_eq!(c.record_project_meta_result("odilo", true), ShouldPause::No); + assert_eq!(c.count("odilo"), 0); + } + + #[test] + fn counter_trips_at_threshold() { + let mut c = ProjectFailureCounter::new(3); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::No + ); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::No + ); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::Yes + ); + } + + #[test] + fn counter_does_not_double_trip_without_success() { + let mut c = ProjectFailureCounter::new(2); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::No + ); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::Yes + ); + // Further failures past the trip do not re-fire. + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::No + ); + } + + #[test] + fn counter_is_per_project() { + let mut c = ProjectFailureCounter::new(2); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::No + ); + assert_eq!( + c.record_project_meta_result("digital-twins", false), + ShouldPause::No + ); + assert_eq!(c.count("odilo"), 1); + assert_eq!(c.count("digital-twins"), 1); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::Yes + ); + assert_eq!( + c.record_project_meta_result("digital-twins", false), + ShouldPause::Yes + ); + } + + #[test] + fn zero_threshold_trips_on_first_failure() { + let mut c = ProjectFailureCounter::new(0); + assert_eq!(c.threshold(), 1); + assert_eq!( + c.record_project_meta_result("odilo", false), + ShouldPause::Yes + ); + } +} diff --git a/crates/terraphim_orchestrator/src/provider_budget.rs b/crates/terraphim_orchestrator/src/provider_budget.rs new file mode 100644 index 000000000..f1c5a4706 --- /dev/null +++ b/crates/terraphim_orchestrator/src/provider_budget.rs @@ -0,0 +1,596 @@ +//! Provider-level spend tracking with hour and day windows. +//! +//! Complements [`crate::cost_tracker::CostTracker`] (which tracks a monthly +//! budget per agent) by tracking accumulated spend per external LLM +//! provider (`opencode-go`, `kimi-for-coding`, ...) in tumbling UTC +//! hour and day windows. Re-uses [`BudgetVerdict`] so the routing +//! engine treats both signals uniformly: `Exhausted` providers are +//! dropped from the candidate set and `NearExhaustion` providers are +//! deprioritised via `BudgetPressure`. +//! +//! Buckets tumble at UTC hour / day boundaries. Missing `max_hour_cents` +//! or `max_day_cents` disables that window (verdict `Uncapped`). +//! +//! State can be snapshotted to a JSON file so a restart does not reset +//! the current-window counters. + +use crate::cost_tracker::BudgetVerdict; +use chrono::{DateTime, Datelike, Timelike, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::io; +use std::path::PathBuf; +use std::sync::Mutex; + +const SUB_CENTS_PER_USD: u64 = 10_000; +const WARNING_THRESHOLD: f64 = 0.80; + +/// Per-provider budget caps. Missing fields disable the corresponding window. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct ProviderBudgetConfig { + /// Provider id (e.g. `opencode-go`, `kimi-for-coding`). + pub id: String, + /// Max spend in cents per UTC hour. `None` = uncapped. + #[serde(default)] + pub max_hour_cents: Option, + /// Max spend in cents per UTC day. `None` = uncapped. + #[serde(default)] + pub max_day_cents: Option, + /// Optional regex patterns for classifying this provider's stderr. + /// Consumed by [`crate::error_signatures`] at config load time. + #[serde(default)] + pub error_signatures: Option, +} + +/// Serialisable window state. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct WindowState { + /// UTC tumbling bucket id: `YYYYMMDDHH` for hour, `YYYYMMDD` for day. + pub window_id: u64, + /// Accumulated spend in hundredths-of-a-cent. + pub sub_cents: u64, +} + +/// Serialisable snapshot of a single provider's two windows. +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProviderSnapshotEntry { + pub hour: WindowState, + pub day: WindowState, +} + +/// Serialisable snapshot of the whole tracker. +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProviderBudgetSnapshot { + pub providers: HashMap, +} + +/// Hour + day windows held behind a single mutex so record and check +/// observe a consistent snapshot across both windows. A prior split +/// (one mutex per window) allowed a concurrent recorder to interleave +/// updates such that an observer between the two locks saw the hour +/// bucket advanced but not the day bucket. +#[derive(Debug, Default)] +struct ProviderWindows { + hour: WindowState, + day: WindowState, +} + +#[derive(Debug, Default)] +struct ProviderState { + windows: Mutex, +} + +/// Tracks provider spend across hour and day windows. +#[derive(Debug)] +pub struct ProviderBudgetTracker { + configs: HashMap, + state: HashMap, + state_file: Option, +} + +impl ProviderBudgetTracker { + /// Build a tracker from the given config list. No persistence. + pub fn new(configs: Vec) -> Self { + let mut config_map = HashMap::new(); + let mut state_map = HashMap::new(); + for cfg in configs { + let id = cfg.id.clone(); + state_map.insert(id.clone(), ProviderState::default()); + config_map.insert(id, cfg); + } + Self { + configs: config_map, + state: state_map, + state_file: None, + } + } + + /// Build a tracker and load any prior snapshot from `state_file`. + /// A missing file is not an error -- the tracker starts empty. + pub fn with_persistence( + configs: Vec, + state_file: PathBuf, + ) -> io::Result { + let mut tracker = Self::new(configs); + tracker.state_file = Some(state_file.clone()); + if state_file.exists() { + let raw = fs::read_to_string(&state_file)?; + if raw.trim().is_empty() { + return Ok(tracker); + } + let snap: ProviderBudgetSnapshot = serde_json::from_str(&raw) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + tracker.apply_snapshot(snap); + } + Ok(tracker) + } + + /// Overlay a snapshot onto an existing tracker. Only providers that + /// are still configured keep their state; the rest are discarded so + /// stale entries from removed providers do not linger. + fn apply_snapshot(&mut self, snap: ProviderBudgetSnapshot) { + for (provider, entry) in snap.providers { + if let Some(state) = self.state.get_mut(&provider) { + let mut w = state.windows.lock().expect("windows lock poisoned"); + w.hour = entry.hour; + w.day = entry.day; + } + } + } + + /// Return a serialisable view of the current state. + pub fn snapshot(&self) -> ProviderBudgetSnapshot { + let mut providers = HashMap::with_capacity(self.state.len()); + for (id, state) in &self.state { + let w = state.windows.lock().expect("windows lock poisoned"); + providers.insert( + id.clone(), + ProviderSnapshotEntry { + hour: w.hour, + day: w.day, + }, + ); + } + ProviderBudgetSnapshot { providers } + } + + /// Persist the current snapshot to `state_file` (if configured). + /// Writes are atomic via a sibling `.tmp` file + rename. + pub fn persist(&self) -> io::Result<()> { + let Some(path) = self.state_file.as_ref() else { + return Ok(()); + }; + let snap = self.snapshot(); + let json = serde_json::to_string_pretty(&snap) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let tmp = path.with_extension("tmp"); + if let Some(parent) = tmp.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&tmp, json)?; + fs::rename(&tmp, path)?; + Ok(()) + } + + /// Record a USD spend against the provider and return the merged + /// hour+day verdict. Unknown providers are silently ignored (return + /// `Uncapped`) -- we only track providers with an explicit config. + pub fn record_cost(&self, provider: &str, cost_usd: f64) -> BudgetVerdict { + self.record_cost_at(provider, cost_usd, Utc::now()) + } + + /// Test hook: record against a caller-supplied timestamp so tests + /// can cross window boundaries deterministically. + pub fn record_cost_at( + &self, + provider: &str, + cost_usd: f64, + now: DateTime, + ) -> BudgetVerdict { + let Some(cfg) = self.configs.get(provider) else { + return BudgetVerdict::Uncapped; + }; + let Some(state) = self.state.get(provider) else { + return BudgetVerdict::Uncapped; + }; + let delta = (cost_usd * SUB_CENTS_PER_USD as f64).round().max(0.0) as u64; + + // Single lock across both windows: record and prune atomically + // so a concurrent `check` cannot observe a half-updated state. + let mut w = state.windows.lock().expect("windows lock poisoned"); + let hour_verdict = + update_window_in_place(&mut w.hour, hour_window_id(now), cfg.max_hour_cents, delta); + let day_verdict = + update_window_in_place(&mut w.day, day_window_id(now), cfg.max_day_cents, delta); + + combine_verdicts(hour_verdict, day_verdict) + } + + /// Non-mutating check. Does NOT record spend; returns the verdict + /// that would apply if a zero-cost call were made right now. + pub fn check(&self, provider: &str) -> BudgetVerdict { + self.check_at(provider, Utc::now()) + } + + /// Test hook for `check` with caller-supplied timestamp. + pub fn check_at(&self, provider: &str, now: DateTime) -> BudgetVerdict { + let Some(cfg) = self.configs.get(provider) else { + return BudgetVerdict::Uncapped; + }; + let Some(state) = self.state.get(provider) else { + return BudgetVerdict::Uncapped; + }; + // Single lock: hour and day observed atomically. + let w = state.windows.lock().expect("windows lock poisoned"); + let hour_verdict = check_window_state(&w.hour, hour_window_id(now), cfg.max_hour_cents); + let day_verdict = check_window_state(&w.day, day_window_id(now), cfg.max_day_cents); + combine_verdicts(hour_verdict, day_verdict) + } + + /// Force both windows for `provider` past their caps so the next + /// [`Self::check`] returns [`BudgetVerdict::Exhausted`]. Used by the + /// error-signature classifier when a spawn stderr signals that the + /// provider has hit its external quota even though our spend counter + /// has not yet registered the charge (providers bill asynchronously). + /// + /// Only providers with at least one configured cap are affected -- + /// uncapped providers remain `Uncapped` because there is nothing to + /// exhaust. Unknown providers are silently ignored. + pub fn force_exhaust(&self, provider: &str) { + let Some(cfg) = self.configs.get(provider) else { + return; + }; + let Some(state) = self.state.get(provider) else { + return; + }; + let now = Utc::now(); + let mut w = state.windows.lock().expect("windows lock poisoned"); + if let Some(cap) = cfg.max_hour_cents { + w.hour.window_id = hour_window_id(now); + w.hour.sub_cents = cap.saturating_mul(100).saturating_add(100); + } + if let Some(cap) = cfg.max_day_cents { + w.day.window_id = day_window_id(now); + w.day.sub_cents = cap.saturating_mul(100).saturating_add(100); + } + } + + /// Iterate over all provider ids known to the tracker. + pub fn providers(&self) -> impl Iterator { + self.configs.keys().map(|s| s.as_str()) + } + + pub fn is_empty(&self) -> bool { + self.configs.is_empty() + } +} + +fn hour_window_id(ts: DateTime) -> u64 { + (ts.year() as u64) * 1_000_000 + + (ts.month() as u64) * 10_000 + + (ts.day() as u64) * 100 + + (ts.hour() as u64) +} + +fn day_window_id(ts: DateTime) -> u64 { + (ts.year() as u64) * 10_000 + (ts.month() as u64) * 100 + (ts.day() as u64) +} + +/// Apply `delta` sub-cents to the window, resetting first if the bucket +/// has rolled over. Returns the verdict that applies post-record. +/// Operates on an already-locked `WindowState` so the caller holds the +/// combined hour+day mutex. +fn update_window_in_place( + ws: &mut WindowState, + current_id: u64, + max_cents: Option, + delta: u64, +) -> BudgetVerdict { + if ws.window_id != current_id { + ws.window_id = current_id; + ws.sub_cents = 0; + } + ws.sub_cents = ws.sub_cents.saturating_add(delta); + verdict_for(ws.sub_cents, max_cents) +} + +fn check_window_state(ws: &WindowState, current_id: u64, max_cents: Option) -> BudgetVerdict { + if ws.window_id != current_id { + // Fresh bucket -- no spend yet. + return verdict_for(0, max_cents); + } + verdict_for(ws.sub_cents, max_cents) +} + +fn verdict_for(sub_cents: u64, max_cents: Option) -> BudgetVerdict { + let Some(cap) = max_cents else { + return BudgetVerdict::Uncapped; + }; + if cap == 0 { + // Zero cap = always exhausted; a misconfiguration, but handled. + return BudgetVerdict::Exhausted { + spent_cents: sub_cents / 100, + budget_cents: 0, + }; + } + // sub_cents is hundredths-of-a-cent. Normalise to cents for the verdict. + let spent_cents = sub_cents / 100; + let cap_sub_cents = cap.saturating_mul(100); + + if sub_cents >= cap_sub_cents { + BudgetVerdict::Exhausted { + spent_cents, + budget_cents: cap, + } + } else if (sub_cents as f64) >= (cap_sub_cents as f64) * WARNING_THRESHOLD { + BudgetVerdict::NearExhaustion { + spent_cents, + budget_cents: cap, + } + } else { + BudgetVerdict::WithinBudget + } +} + +/// Merge two verdicts -- more urgent wins. Exhausted > NearExhaustion > +/// WithinBudget > Uncapped. +fn combine_verdicts(a: BudgetVerdict, b: BudgetVerdict) -> BudgetVerdict { + fn rank(v: &BudgetVerdict) -> u8 { + match v { + BudgetVerdict::Exhausted { .. } => 3, + BudgetVerdict::NearExhaustion { .. } => 2, + BudgetVerdict::WithinBudget => 1, + BudgetVerdict::Uncapped => 0, + } + } + if rank(&a) >= rank(&b) { + a + } else { + b + } +} + +/// Filter helper for the routing engine: returns `true` if the +/// provider is safe to consider, `false` if its verdict is `Exhausted`. +pub fn provider_has_budget(tracker: &ProviderBudgetTracker, provider: &str) -> bool { + !matches!(tracker.check(provider), BudgetVerdict::Exhausted { .. }) +} + +/// Extract the provider-budget key for a `provider/model` string +/// (e.g. `opencode-go/minimax-m2.5` -> `opencode-go`). Bare model +/// names fall back to the Anthropic subscription id (`claude-code`) +/// so any caller that knows only the model can still look up quota. +/// +/// Returns `None` for model strings that cannot be classified. +pub fn provider_key_for_model(provider_or_model: &str) -> Option<&str> { + if let Some((prefix, _)) = provider_or_model.split_once('/') { + return Some(prefix); + } + if crate::config::CLAUDE_CLI_BARE_MODELS.contains(&provider_or_model) { + return Some("claude-code"); + } + if crate::config::ANTHROPIC_BARE_PROVIDERS.contains(&provider_or_model) { + return Some("claude-code"); + } + // Unknown bare identifier -- treat as its own provider. + Some(provider_or_model) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::TimeZone; + + fn cfg(id: &str, hour: Option, day: Option) -> ProviderBudgetConfig { + ProviderBudgetConfig { + id: id.to_string(), + max_hour_cents: hour, + max_day_cents: day, + error_signatures: None, + } + } + + #[test] + fn test_uncapped_for_unknown_provider() { + let t = ProviderBudgetTracker::new(vec![]); + assert_eq!(t.check("missing"), BudgetVerdict::Uncapped); + // Recording against an unknown provider is a no-op. + assert_eq!(t.record_cost("missing", 10.0), BudgetVerdict::Uncapped); + } + + #[test] + fn test_hour_window_exhausts() { + // 100-cent = $1/hour cap. + let t = ProviderBudgetTracker::new(vec![cfg("opencode-go", Some(100), None)]); + let t0 = Utc.with_ymd_and_hms(2026, 4, 19, 10, 0, 0).unwrap(); + + assert_eq!( + t.record_cost_at("opencode-go", 0.50, t0), + BudgetVerdict::WithinBudget + ); + // Now at 80 cents -> near exhaustion. + assert!(matches!( + t.record_cost_at("opencode-go", 0.30, t0), + BudgetVerdict::NearExhaustion { .. } + )); + // Push over the cap. + assert!(matches!( + t.record_cost_at("opencode-go", 0.30, t0), + BudgetVerdict::Exhausted { .. } + )); + } + + #[test] + fn test_hour_window_resets_on_next_hour() { + let t = ProviderBudgetTracker::new(vec![cfg("opencode-go", Some(100), None)]); + let t0 = Utc.with_ymd_and_hms(2026, 4, 19, 10, 30, 0).unwrap(); + let t1 = Utc.with_ymd_and_hms(2026, 4, 19, 11, 5, 0).unwrap(); + + // Exhaust the 10:00 bucket. + let _ = t.record_cost_at("opencode-go", 1.50, t0); + assert!(matches!( + t.check_at("opencode-go", t0), + BudgetVerdict::Exhausted { .. } + )); + + // 11:00 bucket is fresh. + assert_eq!(t.check_at("opencode-go", t1), BudgetVerdict::WithinBudget); + } + + #[test] + fn test_day_cap_independent_of_hour_cap() { + // 100 cents/hour, 150 cents/day -- hitting the daily cap across + // two hours while each hour stays under its per-hour limit. + let t = ProviderBudgetTracker::new(vec![cfg("opencode-go", Some(100), Some(150))]); + let t0 = Utc.with_ymd_and_hms(2026, 4, 19, 10, 0, 0).unwrap(); + let t1 = Utc.with_ymd_and_hms(2026, 4, 19, 11, 0, 0).unwrap(); + + let _ = t.record_cost_at("opencode-go", 0.90, t0); + // Hour bucket at 90c (near), day at 90c (within 150). Merge: NearExhaustion. + assert!(matches!( + t.check_at("opencode-go", t0), + BudgetVerdict::NearExhaustion { .. } + )); + + // Cross into hour 11; daily counter continues to accumulate. + let _ = t.record_cost_at("opencode-go", 0.70, t1); + let verdict = t.check_at("opencode-go", t1); + assert!( + matches!(verdict, BudgetVerdict::Exhausted { .. }), + "day cap should trip even though hour bucket rolled: {:?}", + verdict + ); + } + + #[test] + fn test_force_exhaust_trips_both_windows() { + // Every window that has a cap must end up Exhausted after + // force_exhaust so the routing gate drops the provider until + // the next UTC window rolls -- even though we never recorded + // any cost. + let t = ProviderBudgetTracker::new(vec![cfg("claude-code", Some(500), Some(2000))]); + assert_eq!(t.check("claude-code"), BudgetVerdict::WithinBudget); + + t.force_exhaust("claude-code"); + assert!( + matches!(t.check("claude-code"), BudgetVerdict::Exhausted { .. }), + "force_exhaust must trip the combined verdict" + ); + // And the filter helper that routing uses must reject it. + assert!(!provider_has_budget(&t, "claude-code")); + } + + #[test] + fn test_force_exhaust_is_noop_for_uncapped_provider() { + // No cap means there's nothing to exhaust -- verdict stays + // Uncapped so we don't accidentally block a provider that + // the operator has intentionally left unbounded. + let t = ProviderBudgetTracker::new(vec![cfg("claude-code", None, None)]); + t.force_exhaust("claude-code"); + assert_eq!(t.check("claude-code"), BudgetVerdict::Uncapped); + } + + #[test] + fn test_force_exhaust_ignores_unknown_provider() { + let t = ProviderBudgetTracker::new(vec![cfg("claude-code", Some(100), None)]); + t.force_exhaust("not-a-provider"); // must not panic or poison state + assert_eq!(t.check("claude-code"), BudgetVerdict::WithinBudget); + } + + #[test] + fn test_day_window_resets_next_day() { + let t = ProviderBudgetTracker::new(vec![cfg("opencode-go", None, Some(100))]); + let t0 = Utc.with_ymd_and_hms(2026, 4, 19, 10, 0, 0).unwrap(); + let t1 = Utc.with_ymd_and_hms(2026, 4, 20, 0, 1, 0).unwrap(); + + let _ = t.record_cost_at("opencode-go", 1.50, t0); + assert!(matches!( + t.check_at("opencode-go", t0), + BudgetVerdict::Exhausted { .. } + )); + // Fresh day. + assert_eq!(t.check_at("opencode-go", t1), BudgetVerdict::WithinBudget); + } + + #[test] + fn test_snapshot_round_trip_via_file() { + let tmp = tempfile::NamedTempFile::new().unwrap(); + let path = tmp.path().to_path_buf(); + // Remove the blank file so `with_persistence` treats it as missing. + drop(tmp); + + let configs = vec![cfg("opencode-go", Some(500), Some(2000))]; + let t = ProviderBudgetTracker::with_persistence(configs.clone(), path.clone()).unwrap(); + let now = Utc.with_ymd_and_hms(2026, 4, 19, 10, 0, 0).unwrap(); + let _ = t.record_cost_at("opencode-go", 1.23, now); + t.persist().unwrap(); + + // New tracker loads the snapshot. + let t2 = ProviderBudgetTracker::with_persistence(configs, path.clone()).unwrap(); + let snap = t2.snapshot(); + let entry = snap + .providers + .get("opencode-go") + .expect("provider state persisted"); + assert_eq!(entry.hour.sub_cents, 12_300); + assert_eq!(entry.day.sub_cents, 12_300); + + // Cleanup + let _ = fs::remove_file(&path); + } + + #[test] + fn test_combine_verdicts_picks_worst() { + let hour = BudgetVerdict::NearExhaustion { + spent_cents: 80, + budget_cents: 100, + }; + let day = BudgetVerdict::Exhausted { + spent_cents: 1000, + budget_cents: 1000, + }; + assert!(matches!( + combine_verdicts(hour, day), + BudgetVerdict::Exhausted { .. } + )); + } + + #[test] + fn test_provider_has_budget_helper() { + // Use `record_cost` + `check` at real-now so both land in the + // same hour bucket and the helper's verdict reflects the spend. + let t = ProviderBudgetTracker::new(vec![cfg("p", Some(100), None)]); + assert!(provider_has_budget(&t, "p")); + let _ = t.record_cost("p", 2.00); + assert!(!provider_has_budget(&t, "p")); + } + + #[test] + fn test_stale_snapshot_entry_for_removed_provider_discarded() { + let tmp = tempfile::NamedTempFile::new().unwrap(); + let path = tmp.path().to_path_buf(); + drop(tmp); + + // Persist state for "old-provider". + let t = ProviderBudgetTracker::with_persistence( + vec![cfg("old-provider", Some(100), None)], + path.clone(), + ) + .unwrap(); + let now = Utc.with_ymd_and_hms(2026, 4, 19, 10, 0, 0).unwrap(); + let _ = t.record_cost_at("old-provider", 0.50, now); + t.persist().unwrap(); + + // Reload with a new config that drops "old-provider". + let t2 = ProviderBudgetTracker::with_persistence( + vec![cfg("new-provider", Some(100), None)], + path.clone(), + ) + .unwrap(); + let snap = t2.snapshot(); + assert!(!snap.providers.contains_key("old-provider")); + + let _ = fs::remove_file(&path); + } +} diff --git a/crates/terraphim_orchestrator/src/quickwit.rs b/crates/terraphim_orchestrator/src/quickwit.rs index 19138324b..54c97f7f1 100644 --- a/crates/terraphim_orchestrator/src/quickwit.rs +++ b/crates/terraphim_orchestrator/src/quickwit.rs @@ -3,16 +3,24 @@ //! Feature-gated behind `quickwit` feature flag. //! Provides async log shipping to Quickwit search engine. +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::warn; +use crate::dispatcher::LEGACY_PROJECT_ID; + /// Log document structure for Quickwit ingestion #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct LogDocument { /// ISO 8601 timestamp pub timestamp: String, + /// Project id owning the agent that produced this log. Legacy + /// single-project configs use [`crate::dispatcher::LEGACY_PROJECT_ID`]. + #[serde(default)] + pub project_id: String, /// Log level (INFO, WARN, ERROR) pub level: String, /// Name of the agent @@ -205,6 +213,95 @@ impl QuickwitSink { } } +/// Fleet sink routing [`LogDocument`]s to per-project [`QuickwitSink`]s. +/// +/// Legacy single-project deployments wire a single sink keyed on +/// [`crate::dispatcher::LEGACY_PROJECT_ID`]. Multi-project fleets register +/// one sink per project plus an optional fallback for docs whose +/// `project_id` does not match any registered sink. +#[derive(Debug, Clone)] +pub struct QuickwitFleetSink { + sinks: HashMap, + fallback_project: Option, +} + +impl QuickwitFleetSink { + /// Build a single-project fleet sink keyed on + /// [`crate::dispatcher::LEGACY_PROJECT_ID`]. Used by legacy deployments + /// and by test scaffolding. + pub fn single(sink: QuickwitSink) -> Self { + let mut sinks = HashMap::new(); + sinks.insert(LEGACY_PROJECT_ID.to_string(), sink); + Self { + sinks, + fallback_project: Some(LEGACY_PROJECT_ID.to_string()), + } + } + + /// Build an empty fleet sink that can have project-specific sinks + /// registered via [`QuickwitFleetSink::insert_project`]. + pub fn new_multi() -> Self { + Self { + sinks: HashMap::new(), + fallback_project: None, + } + } + + /// Register a project-specific sink. + pub fn insert_project(&mut self, project_id: impl Into, sink: QuickwitSink) { + let id = project_id.into(); + if self.fallback_project.is_none() { + self.fallback_project = Some(id.clone()); + } + self.sinks.insert(id, sink); + } + + /// Set the fallback project id used when a doc's `project_id` does not + /// match any registered sink. + pub fn set_fallback_project(&mut self, project_id: impl Into) { + self.fallback_project = Some(project_id.into()); + } + + /// Send a log document, routing it to the sink for `doc.project_id`. + /// + /// Falls back to the fallback project sink (or legacy) when no matching + /// sink is registered. Returns `Ok(())` silently when neither exists so + /// that callers can enable Quickwit on a per-project basis without all + /// projects configuring it. + pub async fn send(&self, doc: LogDocument) -> Result<(), QuickwitError> { + if let Some(sink) = self.sinks.get(&doc.project_id) { + return sink.send(doc).await; + } + if let Some(fallback) = self.fallback_project.as_deref() { + if let Some(sink) = self.sinks.get(fallback) { + return sink.send(doc).await; + } + } + // No sink configured for this project and no fallback: drop silently. + Ok(()) + } + + /// Non-blocking variant of [`QuickwitFleetSink::send`]. + pub fn try_send(&self, doc: LogDocument) -> Result<(), QuickwitError> { + if let Some(sink) = self.sinks.get(&doc.project_id) { + return sink.try_send(doc); + } + if let Some(fallback) = self.fallback_project.as_deref() { + if let Some(sink) = self.sinks.get(fallback) { + return sink.try_send(doc); + } + } + Ok(()) + } + + /// Shutdown all underlying sinks, flushing any pending documents. + pub fn shutdown(self) { + for (_, sink) in self.sinks { + sink.shutdown(); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -213,6 +310,7 @@ mod tests { fn test_log_document_serialization() { let doc = LogDocument { timestamp: "2024-01-01T00:00:00Z".to_string(), + project_id: "odilo".to_string(), level: "INFO".to_string(), agent_name: "test-agent".to_string(), layer: "Safety".to_string(), @@ -231,6 +329,7 @@ mod tests { assert!(json.contains("test-agent")); assert!(json.contains("INFO")); assert!(json.contains("gpt-4")); + assert!(json.contains("\"project_id\":\"odilo\"")); // None fields should be skipped assert!(!json.contains("persona")); } @@ -240,4 +339,53 @@ mod tests { let err = QuickwitError::HttpError("connection refused".to_string()); assert_eq!(err.to_string(), "HTTP error: connection refused"); } + + #[tokio::test] + async fn test_fleet_sink_routes_to_registered_project() { + // Point both sinks at non-routable endpoints so the background task + // fails ingest silently; we only verify that send() doesn't error + // and that unknown project ids fall through to the fallback. + let odilo_sink = QuickwitSink::new( + "http://127.0.0.1:1".to_string(), + "odilo-logs".to_string(), + 10, + 60, + ); + let twins_sink = QuickwitSink::new( + "http://127.0.0.1:1".to_string(), + "twins-logs".to_string(), + 10, + 60, + ); + + let mut fleet = QuickwitFleetSink::new_multi(); + fleet.insert_project("odilo", odilo_sink); + fleet.insert_project("digital-twins", twins_sink); + fleet.set_fallback_project("odilo"); + + // Known project: should route without error. + let odilo_doc = LogDocument { + project_id: "odilo".into(), + ..Default::default() + }; + assert!(fleet.send(odilo_doc).await.is_ok()); + + // Unknown project: routes to fallback rather than erroring. + let unknown_doc = LogDocument { + project_id: "unregistered".into(), + ..Default::default() + }; + assert!(fleet.send(unknown_doc).await.is_ok()); + } + + #[tokio::test] + async fn test_fleet_sink_drops_silently_without_fallback() { + let fleet = QuickwitFleetSink::new_multi(); + let doc = LogDocument { + project_id: "whatever".into(), + ..Default::default() + }; + // No sinks registered, no fallback: should succeed silently. + assert!(fleet.send(doc).await.is_ok()); + } } diff --git a/crates/terraphim_orchestrator/src/scheduler.rs b/crates/terraphim_orchestrator/src/scheduler.rs index f5ba0b890..993602648 100644 --- a/crates/terraphim_orchestrator/src/scheduler.rs +++ b/crates/terraphim_orchestrator/src/scheduler.rs @@ -166,6 +166,8 @@ mod tests { pre_check: None, gitea_issue: None, + + project: None, } } diff --git a/crates/terraphim_orchestrator/src/webhook.rs b/crates/terraphim_orchestrator/src/webhook.rs index 0908e74ab..3cd510006 100644 --- a/crates/terraphim_orchestrator/src/webhook.rs +++ b/crates/terraphim_orchestrator/src/webhook.rs @@ -53,6 +53,9 @@ struct GiteaRepository { pub enum WebhookDispatch { SpawnAgent { agent_name: String, + /// Project extracted from a qualified `@adf:project/name` mention, or + /// `None` for unqualified `@adf:name` mentions. + detected_project: Option, issue_number: u64, comment_id: u64, context: String, @@ -157,13 +160,37 @@ async fn handle_gitea_webhook( .collect(); let parser = AdfCommandParser::new(&state.agent_names, &persona_names); + // Parse all mention tokens first to capture project prefixes from qualified + // mentions (`@adf:project/name`) before the Aho-Corasick parser strips them. + let mention_tokens = crate::mention::parse_mention_tokens(&payload.comment.body); + // Map agent_name -> detected project for unqualified mentions resolved by parser. + let unqualified_project_map: std::collections::HashMap> = mention_tokens + .iter() + .filter(|t| t.project.is_none()) + .map(|t| (t.agent.clone(), None)) + .collect(); + let commands = parser.parse_commands( &payload.comment.body, payload.issue.number, payload.comment.id, ); - if commands.is_empty() { + // Collect qualified tokens that AdfCommandParser cannot match (it only knows + // `@adf:{name}` patterns and `@adf:project/name` is not a substring of those). + let qualified_dispatches: Vec = mention_tokens + .into_iter() + .filter(|t| t.project.is_some()) + .map(|t| WebhookDispatch::SpawnAgent { + detected_project: t.project, + agent_name: t.agent, + issue_number: payload.issue.number, + comment_id: payload.comment.id, + context: String::new(), + }) + .collect(); + + if commands.is_empty() && qualified_dispatches.is_empty() { return StatusCode::OK; } @@ -177,6 +204,7 @@ async fn handle_gitea_webhook( comment_id, context, } => WebhookDispatch::SpawnAgent { + detected_project: unqualified_project_map.get(&agent_name).cloned().flatten(), agent_name, issue_number, comment_id, @@ -213,6 +241,15 @@ async fn handle_gitea_webhook( commands_dispatched += 1; } + // Dispatch qualified mentions separately (AdfCommandParser can't see `@adf:proj/name`). + for dispatch in qualified_dispatches { + if let Err(e) = state.dispatch_tx.send(dispatch).await { + warn!(error = %e, "failed to send qualified mention dispatch to orchestrator"); + return StatusCode::SERVICE_UNAVAILABLE; + } + commands_dispatched += 1; + } + info!( repo = %payload.repository.full_name, issue = payload.issue.number, diff --git a/crates/terraphim_orchestrator/tests/adf_check_tests.rs b/crates/terraphim_orchestrator/tests/adf_check_tests.rs new file mode 100644 index 000000000..f0003c497 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/adf_check_tests.rs @@ -0,0 +1,111 @@ +//! End-to-end tests for `adf --check ` dry-run validation. +//! Invokes the compiled binary and asserts on exit code + stdout. + +use std::path::PathBuf; +use std::process::Command; + +fn adf_bin() -> PathBuf { + // `CARGO_BIN_EXE_adf` is set by cargo for the adf integration test target. + PathBuf::from(env!("CARGO_BIN_EXE_adf")) +} + +fn fixture(name: &str) -> PathBuf { + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests"); + p.push("fixtures"); + p.push("multi_project"); + p.push(name); + p +} + +#[test] +fn adf_check_succeeds_on_valid_inline_config() { + let out = Command::new(adf_bin()) + .arg("--check") + .arg(fixture("base_inline.toml")) + .output() + .expect("run adf --check"); + + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(out.status.success(), "expected success, got {out:?}"); + assert!( + stdout.contains("PROJECT"), + "stdout missing header: {stdout}" + ); + assert!( + stdout.contains("alpha-watcher"), + "stdout missing alpha-watcher: {stdout}" + ); + assert!( + stdout.contains("beta-watcher"), + "stdout missing beta-watcher: {stdout}" + ); +} + +#[test] +fn adf_check_expands_include_and_prints_merged_agents() { + let out = Command::new(adf_bin()) + .arg("--check") + .arg(fixture("base_include.toml")) + .output() + .expect("run adf --check"); + + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(out.status.success(), "expected success, got {out:?}"); + // All three agents from the merged fragments must be present. + assert!(stdout.contains("alpha-watcher")); + assert!(stdout.contains("beta-watcher")); + assert!(stdout.contains("beta-reviewer")); + // Model column shows the subscription-allowed model. + assert!(stdout.contains("sonnet")); +} + +#[test] +fn adf_check_fails_on_banned_provider_with_nonzero_exit() { + let out = Command::new(adf_bin()) + .arg("--check") + .arg(fixture("invalid_banned.toml")) + .output() + .expect("run adf --check"); + + assert!(!out.status.success(), "expected failure, got success"); + let code = out.status.code().unwrap_or_default(); + assert_eq!(code, 1, "expected exit code 1, got {code}"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("FAILED") || stderr.contains("google/gemini-2"), + "stderr should mention failure or banned provider: {stderr}" + ); +} + +#[test] +fn adf_check_fails_on_missing_file() { + let out = Command::new(adf_bin()) + .arg("--check") + .arg("/tmp/definitely-does-not-exist-adf-test.toml") + .output() + .expect("run adf --check"); + + assert!(!out.status.success()); + assert_eq!(out.status.code(), Some(1)); +} + +#[test] +fn adf_check_table_is_sorted_by_project_then_agent() { + let out = Command::new(adf_bin()) + .arg("--check") + .arg(fixture("base_include.toml")) + .output() + .expect("run adf --check"); + + let stdout = String::from_utf8_lossy(&out.stdout); + let alpha_idx = stdout.find("alpha-watcher").expect("alpha-watcher present"); + let beta_rev_idx = stdout.find("beta-reviewer").expect("beta-reviewer present"); + let beta_watch_idx = stdout.find("beta-watcher").expect("beta-watcher present"); + + // alpha project rows first. + assert!(alpha_idx < beta_rev_idx); + assert!(alpha_idx < beta_watch_idx); + // within beta, alphabetical: reviewer before watcher. + assert!(beta_rev_idx < beta_watch_idx); +} diff --git a/crates/terraphim_orchestrator/tests/error_signatures_tests.rs b/crates/terraphim_orchestrator/tests/error_signatures_tests.rs new file mode 100644 index 000000000..336ce772e --- /dev/null +++ b/crates/terraphim_orchestrator/tests/error_signatures_tests.rs @@ -0,0 +1,295 @@ +//! Integration tests for per-provider error-signature classification +//! (Refs terraphim/adf-fleet#7). +//! +//! Each fixture under `tests/fixtures/stderr/` captures a realistic +//! stderr snippet from one of the subscription-only providers we support: +//! claude-code, opencode-go, zai-coding-plan, kimi-for-coding. +//! +//! These tests exercise the real classifier with the regex lists an +//! operator would ship in `orchestrator.toml`. No mocks. + +use std::fs; +use std::path::{Path, PathBuf}; + +use terraphim_orchestrator::error_signatures::{ + self, CompiledSignatures, ErrorKind, ProviderErrorSignatures, +}; +use terraphim_orchestrator::provider_budget::ProviderBudgetConfig; + +fn fixture_path(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("stderr") + .join(name) +} + +fn read_fixture(name: &str) -> String { + fs::read_to_string(fixture_path(name)) + .unwrap_or_else(|e| panic!("fixture `{}` must exist: {}", name, e)) +} + +/// The canonical set of regex lists we'd ship in `orchestrator.toml`. +/// Kept as a single source of truth so fixture expectations and the +/// realistic config stay in lock-step. +fn canonical_sigs(provider: &str) -> ProviderErrorSignatures { + match provider { + "claude-code" => ProviderErrorSignatures { + throttle: vec![ + "rate.?limit".into(), + "429".into(), + "usage limit".into(), + "rate_limit_error".into(), + ], + flake: vec![ + "timed out".into(), + "timeout".into(), + "connection reset".into(), + "eof".into(), + ], + }, + "opencode-go" => ProviderErrorSignatures { + throttle: vec!["rate limit".into(), "429".into(), "quota".into()], + flake: vec![ + "i/o timeout".into(), + "connection reset".into(), + "unexpected eof".into(), + ], + }, + "zai-coding-plan" => ProviderErrorSignatures { + throttle: vec![ + "insufficient.?balance".into(), + "quota".into(), + "rate.?limit".into(), + ], + flake: vec![ + "read timeout".into(), + "connection reset".into(), + "session aborted due to timeout".into(), + ], + }, + "kimi-for-coding" => ProviderErrorSignatures { + throttle: vec!["quota.?exceeded".into(), "quota exceeded".into()], + flake: vec!["eof".into(), "closed connection".into()], + }, + other => panic!("unexpected provider in test fixture: {}", other), + } +} + +fn compile(provider: &str) -> CompiledSignatures { + CompiledSignatures::compile(provider, &canonical_sigs(provider)) + .expect("canonical test signatures must compile") +} + +#[test] +fn claude_429_classifies_as_throttle() { + let sigs = compile("claude-code"); + assert_eq!( + error_signatures::classify(&read_fixture("claude_429.txt"), Some(&sigs)), + ErrorKind::Throttle + ); +} + +#[test] +fn claude_usage_limit_classifies_as_throttle() { + let sigs = compile("claude-code"); + assert_eq!( + error_signatures::classify(&read_fixture("claude_usage_limit.txt"), Some(&sigs)), + ErrorKind::Throttle + ); +} + +#[test] +fn claude_timeout_classifies_as_flake() { + let sigs = compile("claude-code"); + assert_eq!( + error_signatures::classify(&read_fixture("claude_timeout.txt"), Some(&sigs)), + ErrorKind::Flake + ); +} + +#[test] +fn opencode_go_rate_limit_classifies_as_throttle() { + let sigs = compile("opencode-go"); + assert_eq!( + error_signatures::classify(&read_fixture("opencode_go_rate_limit.txt"), Some(&sigs)), + ErrorKind::Throttle + ); +} + +#[test] +fn opencode_go_timeout_classifies_as_flake() { + let sigs = compile("opencode-go"); + assert_eq!( + error_signatures::classify(&read_fixture("opencode_go_timeout.txt"), Some(&sigs)), + ErrorKind::Flake + ); +} + +#[test] +fn zai_insufficient_balance_classifies_as_throttle() { + let sigs = compile("zai-coding-plan"); + assert_eq!( + error_signatures::classify(&read_fixture("zai_insufficient_balance.txt"), Some(&sigs)), + ErrorKind::Throttle + ); +} + +#[test] +fn zai_glm5_timeout_classifies_as_flake() { + let sigs = compile("zai-coding-plan"); + assert_eq!( + error_signatures::classify(&read_fixture("zai_glm5_timeout.txt"), Some(&sigs)), + ErrorKind::Flake + ); +} + +#[test] +fn kimi_quota_classifies_as_throttle() { + let sigs = compile("kimi-for-coding"); + assert_eq!( + error_signatures::classify(&read_fixture("kimi_quota.txt"), Some(&sigs)), + ErrorKind::Throttle + ); +} + +#[test] +fn kimi_eof_classifies_as_flake() { + let sigs = compile("kimi-for-coding"); + assert_eq!( + error_signatures::classify(&read_fixture("kimi_eof.txt"), Some(&sigs)), + ErrorKind::Flake + ); +} + +#[test] +fn unknown_stderr_classifies_as_unknown_across_all_providers() { + // A panic trace the upstream CLIs never emit in practice must fall + // through both lists for every configured provider so the + // orchestrator escalates it for human review. + let stderr = read_fixture("unknown_error.txt"); + for provider in [ + "claude-code", + "opencode-go", + "zai-coding-plan", + "kimi-for-coding", + ] { + let sigs = compile(provider); + assert_eq!( + error_signatures::classify(&stderr, Some(&sigs)), + ErrorKind::Unknown, + "provider {} should not match panic fixture", + provider + ); + } +} + +#[test] +fn build_signature_map_round_trips_canonical_config() { + // Construct the same [[providers]] block an operator would ship and + // round-trip it through the public build_signature_map API to + // confirm compilation + lookup wiring. + let configs: Vec = [ + "claude-code", + "opencode-go", + "zai-coding-plan", + "kimi-for-coding", + ] + .into_iter() + .map(|id| ProviderBudgetConfig { + id: id.to_string(), + error_signatures: Some(canonical_sigs(id)), + ..Default::default() + }) + .collect(); + + let map = + error_signatures::build_signature_map(&configs).expect("canonical config must compile"); + assert_eq!(map.len(), 4); + + // Spot-check: each provider's fixture still classifies correctly + // through the map-lookup path (what the orchestrator actually does). + let cases = [ + ("claude-code", "claude_429.txt", ErrorKind::Throttle), + ("claude-code", "claude_timeout.txt", ErrorKind::Flake), + ( + "opencode-go", + "opencode_go_rate_limit.txt", + ErrorKind::Throttle, + ), + ( + "zai-coding-plan", + "zai_insufficient_balance.txt", + ErrorKind::Throttle, + ), + ("kimi-for-coding", "kimi_eof.txt", ErrorKind::Flake), + ]; + for (provider, fixture, want) in cases { + let sigs = map.get(provider); + let got = error_signatures::classify(&read_fixture(fixture), sigs); + assert_eq!(got, want, "{}:{}", provider, fixture); + } +} + +#[test] +fn missing_provider_in_map_classifies_as_unknown() { + // A provider that isn't in the map (e.g. signatures accidentally + // omitted from the operator's config) must fall back to Unknown + // rather than silently treat the run as a throttle or a flake. + let configs = vec![ProviderBudgetConfig { + id: "claude-code".to_string(), + error_signatures: Some(canonical_sigs("claude-code")), + ..Default::default() + }]; + let map = error_signatures::build_signature_map(&configs).unwrap(); + + let stderr = read_fixture("zai_insufficient_balance.txt"); + let sigs = map.get("zai-coding-plan"); // never configured + assert_eq!( + error_signatures::classify(&stderr, sigs), + ErrorKind::Unknown + ); +} + +#[test] +fn throttle_beats_flake_when_stderr_contains_both() { + // Real captured scenario: a 429 message that also mentions timeout + // ("timeout waiting for rate-limit reset"). Must classify as + // Throttle so we trip the breaker instead of retrying blindly. + let mixed = "timeout waiting for rate-limit reset; retry-after 30s"; + let sigs = compile("claude-code"); + assert_eq!( + error_signatures::classify(mixed, Some(&sigs)), + ErrorKind::Throttle + ); +} + +#[test] +fn dedupe_key_collapses_minor_shape_variance() { + // Two stderr shapes from the same underlying failure -- trailing + // newline + case variance + extra detail -- must hash to one key + // so we don't open duplicate fleet-meta issues. + let a = " UPSTREAM CLOSED CONNECTION, EOF BEFORE COMPLETION\n"; + let b = "upstream closed connection, eof before completion (sess 3)"; + let ka = error_signatures::unknown_dedupe_key("kimi-for-coding", a); + let kb = error_signatures::unknown_dedupe_key("kimi-for-coding", b); + assert_eq!(ka, kb); + assert!(ka.starts_with("kimi-for-coding::")); +} + +#[test] +fn classify_lines_matches_live_stderr_capture_pattern() { + // The orchestrator captures stderr line-by-line via + // ManagedAgent::output_rx and feeds classify_lines directly. + // Mirror that exact call shape here. + let sigs = compile("opencode-go"); + let lines: Vec = read_fixture("opencode_go_rate_limit.txt") + .lines() + .map(|s| s.to_string()) + .collect(); + assert!(lines.len() >= 3, "fixture must exercise multi-line join"); + assert_eq!( + error_signatures::classify_lines(&lines, Some(&sigs)), + ErrorKind::Throttle + ); +} diff --git a/crates/terraphim_orchestrator/tests/fixtures/multi_project/base_include.toml b/crates/terraphim_orchestrator/tests/fixtures/multi_project/base_include.toml new file mode 100644 index 000000000..4ab854db9 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/multi_project/base_include.toml @@ -0,0 +1,8 @@ +working_dir = "/tmp/terraphim" +include = ["conf.d/*.toml"] + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" diff --git a/crates/terraphim_orchestrator/tests/fixtures/multi_project/base_inline.toml b/crates/terraphim_orchestrator/tests/fixtures/multi_project/base_inline.toml new file mode 100644 index 000000000..2865c0725 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/multi_project/base_inline.toml @@ -0,0 +1,31 @@ +working_dir = "/tmp/terraphim" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" +schedule_offset_minutes = 0 + +[[projects]] +id = "beta" +working_dir = "/tmp/beta" +schedule_offset_minutes = 5 + +[[agents]] +name = "alpha-watcher" +layer = "Safety" +cli_tool = "claude" +task = "Watch alpha" +project = "alpha" + +[[agents]] +name = "beta-watcher" +layer = "Safety" +cli_tool = "claude" +task = "Watch beta" +project = "beta" diff --git a/crates/terraphim_orchestrator/tests/fixtures/multi_project/conf.d/alpha.toml b/crates/terraphim_orchestrator/tests/fixtures/multi_project/conf.d/alpha.toml new file mode 100644 index 000000000..27f0d7e98 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/multi_project/conf.d/alpha.toml @@ -0,0 +1,21 @@ +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" +schedule_offset_minutes = 0 + +[[agents]] +name = "alpha-watcher" +layer = "Safety" +cli_tool = "claude" +task = "Watch alpha" +project = "alpha" + +[[flows]] +name = "alpha-flow" +project = "alpha" +repo_path = "/tmp/alpha" + +[[flows.steps]] +name = "build" +kind = "action" +command = "cargo build" diff --git a/crates/terraphim_orchestrator/tests/fixtures/multi_project/conf.d/beta.toml b/crates/terraphim_orchestrator/tests/fixtures/multi_project/conf.d/beta.toml new file mode 100644 index 000000000..2e060edcb --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/multi_project/conf.d/beta.toml @@ -0,0 +1,19 @@ +[[projects]] +id = "beta" +working_dir = "/tmp/beta" +schedule_offset_minutes = 5 + +[[agents]] +name = "beta-watcher" +layer = "Safety" +cli_tool = "claude" +task = "Watch beta" +project = "beta" + +[[agents]] +name = "beta-reviewer" +layer = "Core" +cli_tool = "claude" +task = "Review beta" +project = "beta" +model = "sonnet" diff --git a/crates/terraphim_orchestrator/tests/fixtures/multi_project/invalid_banned.toml b/crates/terraphim_orchestrator/tests/fixtures/multi_project/invalid_banned.toml new file mode 100644 index 000000000..e022847f3 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/multi_project/invalid_banned.toml @@ -0,0 +1,19 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "p" +working_dir = "/tmp/p" + +[[agents]] +name = "bad-agent" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "p" +model = "google/gemini-2" diff --git a/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/banned_fallback.toml b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/banned_fallback.toml new file mode 100644 index 000000000..0b9d4cd04 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/banned_fallback.toml @@ -0,0 +1,20 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "p" +working_dir = "/tmp/p" + +[[agents]] +name = "bad-fallback-agent" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "p" +model = "sonnet" +fallback_model = "opencode/claude-haiku" diff --git a/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/banned_provider.toml b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/banned_provider.toml new file mode 100644 index 000000000..cc5242953 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/banned_provider.toml @@ -0,0 +1,19 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "p" +working_dir = "/tmp/p" + +[[agents]] +name = "bad-agent" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "p" +model = "opencode/claude-sonnet" diff --git a/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/duplicate_project_id.toml b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/duplicate_project_id.toml new file mode 100644 index 000000000..09124c390 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/duplicate_project_id.toml @@ -0,0 +1,22 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha2" + +[[agents]] +name = "agent-a" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "alpha" diff --git a/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/mixed_mode.toml b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/mixed_mode.toml new file mode 100644 index 000000000..be7fe9d1e --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/mixed_mode.toml @@ -0,0 +1,24 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[agents]] +name = "bound-agent" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "alpha" + +[[agents]] +name = "unbound-agent" +layer = "Safety" +cli_tool = "claude" +task = "t" diff --git a/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/mixed_mode_flow.toml b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/mixed_mode_flow.toml new file mode 100644 index 000000000..7cab503b3 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/mixed_mode_flow.toml @@ -0,0 +1,23 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[agents]] +name = "legacy-agent" +layer = "Safety" +cli_tool = "claude" +task = "t" + +[[flows]] +name = "orphan-flow" +project = "nonexistent" +repo_path = "/tmp/repo" + +[[flows.steps]] +name = "build" +kind = "action" +command = "cargo build" diff --git a/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/unknown_project_ref.toml b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/unknown_project_ref.toml new file mode 100644 index 000000000..19d398db6 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/unknown_project_ref.toml @@ -0,0 +1,18 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[agents]] +name = "ghost-agent" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "nonexistent" diff --git a/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/valid_legacy.toml b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/valid_legacy.toml new file mode 100644 index 000000000..d0797b9b0 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/valid_legacy.toml @@ -0,0 +1,13 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[agents]] +name = "legacy-sentinel" +layer = "Safety" +cli_tool = "claude" +task = "Watch everything" diff --git a/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/valid_multi_project.toml b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/valid_multi_project.toml new file mode 100644 index 000000000..2703601b6 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/runtime_validate/valid_multi_project.toml @@ -0,0 +1,31 @@ +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[projects]] +id = "beta" +working_dir = "/tmp/beta" + +[[agents]] +name = "alpha-sentinel" +layer = "Safety" +cli_tool = "claude" +task = "Watch alpha" +project = "alpha" +model = "sonnet" + +[[agents]] +name = "beta-sentinel" +layer = "Safety" +cli_tool = "claude" +task = "Watch beta" +project = "beta" +model = "opus" diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_429.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_429.txt new file mode 100644 index 000000000..abf8cc7db --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_429.txt @@ -0,0 +1,4 @@ +error: received HTTP 429 Too Many Requests from Anthropic API +retry-after: 60 +request_id: req_01HZ4K7M9NQ8X2Y5P3W1V0T6 +{"type":"error","error":{"type":"rate_limit_error","message":"Number of request tokens has exceeded your per-minute rate limit. Please try again in 60 seconds."}} diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_timeout.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_timeout.txt new file mode 100644 index 000000000..36520ecb1 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_timeout.txt @@ -0,0 +1,3 @@ +error: streaming response timed out after 90s while awaiting next event +io::Error { kind: TimedOut, source: "read timed out" } +CLI aborting, retry recommended. diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_usage_limit.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_usage_limit.txt new file mode 100644 index 000000000..0a9f4c9cf --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/claude_usage_limit.txt @@ -0,0 +1,4 @@ +Error: usage limit reached for current billing period +Your organization has exceeded its monthly message allowance. Contact your +workspace administrator or upgrade your plan at https://console.anthropic.com/ +CLI exiting with code 1. diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/kimi_eof.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/kimi_eof.txt new file mode 100644 index 000000000..b65522a3b --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/kimi_eof.txt @@ -0,0 +1,3 @@ +kimi-for-coding: upstream closed connection, EOF before completion +warn: socket read returned 0 bytes after 38s +retrying in 5s diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/kimi_quota.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/kimi_quota.txt new file mode 100644 index 000000000..b73beaa7a --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/kimi_quota.txt @@ -0,0 +1,3 @@ +kimi-for-coding v2.3.1 +error: daily quota exceeded for subscription plan k2p5 +{"error":{"code":"quota_exceeded","message":"You have hit your daily message quota. It resets at 00:00 UTC."}} diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/opencode_go_rate_limit.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/opencode_go_rate_limit.txt new file mode 100644 index 000000000..ed8bb42eb --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/opencode_go_rate_limit.txt @@ -0,0 +1,3 @@ +2026/04/20 11:58:03 provider minimax: rate limit exceeded for subscription plan +2026/04/20 11:58:03 request 3f9c blocked by upstream rate limiter +opencode-go: error: got 429 from provider, retry after 45s diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/opencode_go_timeout.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/opencode_go_timeout.txt new file mode 100644 index 000000000..2308cd8a3 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/opencode_go_timeout.txt @@ -0,0 +1,3 @@ +2026/04/20 11:59:12 dial tcp 203.0.113.7:443: i/o timeout +2026/04/20 11:59:12 opencode-go: connection reset by peer while streaming +unexpected EOF from provider after 42s, aborting session diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/unknown_error.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/unknown_error.txt new file mode 100644 index 000000000..93010351a --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/unknown_error.txt @@ -0,0 +1,3 @@ +panic: internal assertion failed: received malformed message envelope +thread 'main' panicked at src/session.rs:412:9 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/zai_glm5_timeout.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/zai_glm5_timeout.txt new file mode 100644 index 000000000..441e90492 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/zai_glm5_timeout.txt @@ -0,0 +1,4 @@ +warn: glm-4.5-air upstream read timeout after 75s +warn: connection reset by peer (glm-pool-07) +retrying with next pool entry... +error: session aborted due to timeout, session_id=sess_9f3a diff --git a/crates/terraphim_orchestrator/tests/fixtures/stderr/zai_insufficient_balance.txt b/crates/terraphim_orchestrator/tests/fixtures/stderr/zai_insufficient_balance.txt new file mode 100644 index 000000000..33b08d371 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/fixtures/stderr/zai_insufficient_balance.txt @@ -0,0 +1,2 @@ +Error contacting z.ai: {"code":"InsufficientBalance","message":"Insufficient balance on your subscription account. Please top up at console.z.ai/billing to continue using GLM models."} +zai-coding-plan exiting with status 1 diff --git a/crates/terraphim_orchestrator/tests/mention_multi_repo_tests.rs b/crates/terraphim_orchestrator/tests/mention_multi_repo_tests.rs new file mode 100644 index 000000000..b0b7e02b8 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/mention_multi_repo_tests.rs @@ -0,0 +1,414 @@ +//! Integration tests for multi-repo mention routing (issue #5). +//! +//! Covers: +//! - Extended `MENTION_RE` regex capturing optional `/` prefix. +//! - Project-aware `resolve_mention` resolution rules. +//! - `parse_mentions` stamping `project_id` onto detected mentions. +//! - `MentionCursor` per-project key isolation at the API level. +//! - One-shot `migrate_legacy_mention_cursor` idempotency. + +use terraphim_orchestrator::config::{AgentDefinition, AgentLayer, Project}; +use terraphim_orchestrator::mention::{ + migrate_legacy_mention_cursor, parse_mention_tokens, parse_mentions, resolve_mention, + MentionCursor, +}; +use terraphim_orchestrator::persona::PersonaRegistry; +use terraphim_tracker::{CommentUser, IssueComment}; + +const LEGACY: &str = "__global__"; + +fn agent(name: &str, project: Option<&str>) -> AgentDefinition { + AgentDefinition { + name: name.to_string(), + layer: AgentLayer::Growth, + cli_tool: "echo".to_string(), + task: "t".to_string(), + schedule: None, + model: None, + capabilities: vec![], + max_memory_bytes: None, + budget_monthly_cents: None, + provider: None, + persona: None, + terraphim_role: None, + skill_chain: vec![], + sfia_skills: vec![], + fallback_provider: None, + fallback_model: None, + grace_period_secs: None, + max_cpu_seconds: None, + pre_check: None, + gitea_issue: None, + project: project.map(|s| s.to_string()), + } +} + +fn comment(id: u64, body: &str) -> IssueComment { + IssueComment { + id, + issue_number: 0, + body: body.to_string(), + user: CommentUser { + login: "tester".to_string(), + }, + created_at: "2026-04-19T00:00:00Z".to_string(), + updated_at: "2026-04-19T00:00:00Z".to_string(), + } +} + +// --------------------------------------------------------------------------- +// Regex: parse_mention_tokens +// --------------------------------------------------------------------------- + +#[test] +fn regex_captures_unqualified_mention() { + let tokens = parse_mention_tokens("hello @adf:developer please"); + assert_eq!(tokens.len(), 1); + assert!(tokens[0].project.is_none()); + assert_eq!(tokens[0].agent, "developer"); +} + +#[test] +fn regex_captures_qualified_mention() { + let tokens = parse_mention_tokens("hello @adf:odilo/developer please"); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].project.as_deref(), Some("odilo")); + assert_eq!(tokens[0].agent, "developer"); +} + +#[test] +fn regex_mixes_qualified_and_unqualified_in_one_comment() { + let tokens = + parse_mention_tokens("@adf:security @adf:odilo/reviewer and @adf:terraphim/cartoprist"); + let names: Vec<(Option<&str>, &str)> = tokens + .iter() + .map(|t| (t.project.as_deref(), t.agent.as_str())) + .collect(); + assert_eq!( + names, + vec![ + (None, "security"), + (Some("odilo"), "reviewer"), + (Some("terraphim"), "cartoprist"), + ] + ); +} + +#[test] +fn regex_rejects_uppercase_project_prefix() { + // Uppercase is not allowed in the project prefix and the agent name + // also requires a lowercase start, so `@adf:Odilo/developer` produces + // no tokens at all. + let tokens = parse_mention_tokens("see @adf:Odilo/developer"); + assert!( + tokens.is_empty(), + "uppercase prefix must not be captured, got {tokens:?}" + ); +} + +#[test] +fn regex_rejects_too_long_project_prefix() { + // 41-char project prefix exceeds the {1,39} cap (min 2-char start + 39) + let long = "a".repeat(41); + let text = format!("@adf:{long}/dev"); + let tokens = parse_mention_tokens(&text); + // Fallback behaviour: regex may still match `dev` unqualified — the + // important assertion is that nothing is captured as a qualified + // mention with the over-long project. + for t in &tokens { + assert_ne!(t.project.as_deref(), Some(long.as_str())); + } +} + +#[test] +fn regex_handles_trailing_punctuation() { + let tokens = parse_mention_tokens("ping @adf:odilo/reviewer, thanks!"); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].project.as_deref(), Some("odilo")); + assert_eq!(tokens[0].agent, "reviewer"); +} + +#[test] +fn regex_ignores_plain_at_mentions() { + let tokens = parse_mention_tokens("@alex please review @odilo/developer too"); + assert!(tokens.is_empty()); +} + +// --------------------------------------------------------------------------- +// resolve_mention: project-aware resolution +// --------------------------------------------------------------------------- + +#[test] +fn resolve_mention_qualified_exact_match() { + let agents = vec![ + agent("developer", Some("odilo")), + agent("developer", Some("terraphim")), + ]; + let resolved = resolve_mention(Some("odilo"), "terraphim", "developer", &agents).unwrap(); + assert_eq!(resolved.name, "developer"); + assert_eq!(resolved.project.as_deref(), Some("odilo")); +} + +#[test] +fn resolve_mention_qualified_not_found_returns_none() { + let agents = vec![agent("developer", Some("odilo"))]; + // Ask for a project the agent doesn't belong to. + let resolved = resolve_mention(Some("terraphim"), "terraphim", "developer", &agents); + assert!(resolved.is_none()); +} + +#[test] +fn resolve_mention_qualified_ambiguous_returns_none() { + // Two agents with the same name AND project — should be impossible + // at config-validation time, but resolver must still refuse. + let agents = vec![ + agent("developer", Some("odilo")), + agent("developer", Some("odilo")), + ]; + let resolved = resolve_mention(Some("odilo"), LEGACY, "developer", &agents); + assert!(resolved.is_none()); +} + +#[test] +fn resolve_mention_unqualified_legacy_matches_any() { + // Legacy single-project mode: ignore agent's project field entirely. + let agents = vec![agent("developer", None), agent("reviewer", Some("odilo"))]; + let resolved = resolve_mention(None, LEGACY, "reviewer", &agents).unwrap(); + assert_eq!(resolved.name, "reviewer"); + assert_eq!(resolved.project.as_deref(), Some("odilo")); +} + +#[test] +fn resolve_mention_unqualified_prefers_hinted_project() { + let agents = vec![ + agent("developer", Some("odilo")), + agent("developer", Some("terraphim")), + ]; + // Polling odilo's repo → the odilo developer wins. + let resolved = resolve_mention(None, "odilo", "developer", &agents).unwrap(); + assert_eq!(resolved.project.as_deref(), Some("odilo")); + + // Polling terraphim's repo → the terraphim developer wins. + let resolved = resolve_mention(None, "terraphim", "developer", &agents).unwrap(); + assert_eq!(resolved.project.as_deref(), Some("terraphim")); +} + +#[test] +fn resolve_mention_unqualified_falls_back_to_unbound() { + // No agent bound to the hinted project — fall back to a + // project-less agent of the same name. + let agents = vec![agent("developer", Some("odilo")), agent("floater", None)]; + let resolved = resolve_mention(None, "terraphim", "floater", &agents).unwrap(); + assert_eq!(resolved.name, "floater"); + assert!(resolved.project.is_none()); +} + +#[test] +fn resolve_mention_unqualified_ambiguous_hinted_returns_none() { + // Two agents, same name, same hinted project → ambiguous. + let agents = vec![ + agent("developer", Some("odilo")), + agent("developer", Some("odilo")), + ]; + let resolved = resolve_mention(None, "odilo", "developer", &agents); + assert!(resolved.is_none()); +} + +#[test] +fn resolve_mention_unqualified_ambiguous_unbound_returns_none() { + // No hinted match; two unbound agents with the same name → ambiguous. + let agents = vec![agent("developer", None), agent("developer", None)]; + let resolved = resolve_mention(None, "odilo", "developer", &agents); + assert!(resolved.is_none()); +} + +#[test] +fn resolve_mention_unqualified_no_match_returns_none() { + let agents = vec![agent("developer", Some("odilo"))]; + let resolved = resolve_mention(None, "terraphim", "ghost", &agents); + assert!(resolved.is_none()); +} + +// --------------------------------------------------------------------------- +// parse_mentions: project_id stamping +// --------------------------------------------------------------------------- + +#[test] +fn parse_mentions_stamps_legacy_project_id() { + let agents = vec![agent("developer", None)]; + let personas = PersonaRegistry::default(); + let c = comment(42, "@adf:developer please look"); + let mentions = parse_mentions(&c, 7, &agents, &personas, LEGACY); + assert_eq!(mentions.len(), 1); + assert_eq!(mentions[0].project_id, LEGACY); + assert_eq!(mentions[0].agent_name, "developer"); +} + +#[test] +fn parse_mentions_stamps_hinted_project_id() { + let agents = vec![agent("developer", Some("odilo"))]; + let personas = PersonaRegistry::default(); + let c = comment(43, "@adf:developer please look"); + let mentions = parse_mentions(&c, 8, &agents, &personas, "odilo"); + assert_eq!(mentions.len(), 1); + assert_eq!(mentions[0].project_id, "odilo"); +} + +// --------------------------------------------------------------------------- +// MentionCursor: in-memory isolation (no persistence needed) +// --------------------------------------------------------------------------- + +#[test] +fn cursor_per_project_isolation() { + // Two cursors are independent structs — `mark_processed` on one + // does not affect the other. + let mut c_odilo = MentionCursor::now(); + let mut c_terra = MentionCursor::now(); + c_odilo.mark_processed(100); + c_terra.mark_processed(200); + assert!(c_odilo.is_processed(100)); + assert!(!c_odilo.is_processed(200)); + assert!(c_terra.is_processed(200)); + assert!(!c_terra.is_processed(100)); +} + +#[test] +fn cursor_advance_to_monotonic() { + let mut c = MentionCursor { + last_seen_at: "2026-04-19T10:00:00Z".to_string(), + dispatches_this_tick: 0, + processed_comment_ids: Default::default(), + }; + // Older timestamp — should NOT regress. + c.advance_to("2026-04-19T09:00:00Z"); + assert_eq!(c.last_seen_at, "2026-04-19T10:00:00Z"); + // Newer timestamp — should advance. + c.advance_to("2026-04-19T11:00:00Z"); + assert_eq!(c.last_seen_at, "2026-04-19T11:00:00Z"); +} + +// --------------------------------------------------------------------------- +// migrate_legacy_mention_cursor: no-op idempotency in memory-only test env +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn migration_is_noop_without_sqlite_backend() { + // In the test environment DeviceStorage uses the memory backend only; + // the sqlite operator is absent, so migration must be a safe no-op + // rather than panicking. + let projects: Vec = vec![]; + migrate_legacy_mention_cursor(&projects).await; + // Calling twice must remain a no-op. + migrate_legacy_mention_cursor(&projects).await; +} + +// --------------------------------------------------------------------------- +// Dispatch wiring: parse_mention_tokens + resolve_mention end-to-end +// +// These tests mirror the logic now wired into the poll and webhook dispatch +// paths. They verify the exact sequence the orchestrator executes for each +// comment body, ensuring qualified `@adf:project/name` mentions route to the +// correct project-scoped agent and unqualified mentions in multi-project mode +// prefer the hinted-project agent. +// --------------------------------------------------------------------------- + +/// Simulates the qualified-mention pass added to `poll_mentions_for_project`. +/// A comment containing `@adf:odilo/developer` must route to the odilo-scoped +/// agent even when two `developer` agents exist in different projects. +#[test] +fn dispatch_wiring_qualified_mention_routes_to_correct_project() { + let agents = vec![ + agent("developer", Some("odilo")), + agent("developer", Some("terraphim")), + ]; + + let comment_body = "Please @adf:odilo/developer review this PR"; + let project_id = "terraphim"; // the poll is running for the terraphim project + + // This is the exact sequence poll_mentions_for_project now executes: + // 1. parse_mention_tokens for qualified mentions + // 2. For each qualified token, call resolve_mention with detected project + let resolved: Vec<_> = parse_mention_tokens(comment_body) + .into_iter() + .filter(|t| t.project.is_some()) + .filter_map(|t| { + let proj = t.project.as_deref(); + resolve_mention(proj, project_id, &t.agent, &agents) + }) + .collect(); + + assert_eq!(resolved.len(), 1, "exactly one agent should be resolved"); + assert_eq!(resolved[0].name, "developer"); + assert_eq!( + resolved[0].project.as_deref(), + Some("odilo"), + "must resolve to the odilo-scoped developer, not the terraphim one" + ); +} + +/// Simulates the unqualified-mention dispatch path (AdfCommandParser produces +/// `agent_name = "developer"`, no detected_project). In multi-project mode the +/// hinted project_id must select the matching agent. +#[test] +fn dispatch_wiring_unqualified_mention_prefers_hinted_project() { + let agents = vec![ + agent("developer", Some("odilo")), + agent("developer", Some("terraphim")), + ]; + + // The AdfCommandParser produces agent_name = "developer" with no project prefix. + let agent_name = "developer"; + let project_id = "odilo"; // poll is running for odilo + + // This is the exact call now used at the SpawnAgent arm in poll_mentions_for_project. + let resolved = resolve_mention(None, project_id, agent_name, &agents); + + assert!( + resolved.is_some(), + "should resolve the hinted-project agent" + ); + assert_eq!( + resolved.unwrap().project.as_deref(), + Some("odilo"), + "must select the odilo-scoped developer" + ); +} + +/// Simulates the webhook dispatch path: qualified mention carried through +/// `WebhookDispatch::SpawnAgent.detected_project` is now resolved correctly. +#[test] +fn dispatch_wiring_webhook_qualified_mention_resolves_by_detected_project() { + let agents = vec![ + agent("reviewer", Some("odilo")), + agent("reviewer", Some("terraphim")), + ]; + + // webhook.rs now extracts detected_project via parse_mention_tokens + let comment_body = "@adf:terraphim/reviewer please check"; + let detected_project = parse_mention_tokens(comment_body) + .into_iter() + .find(|t| t.agent == "reviewer") + .and_then(|t| t.project); + + assert_eq!( + detected_project.as_deref(), + Some("terraphim"), + "webhook handler must extract the project prefix" + ); + + // handle_webhook_dispatch now calls resolve_mention with detected_project + // and LEGACY_PROJECT_ID as the hinted project (webhook has no repo hint). + let resolved = resolve_mention( + detected_project.as_deref(), + "__global__", + "reviewer", + &agents, + ); + + assert!(resolved.is_some()); + assert_eq!( + resolved.unwrap().project.as_deref(), + Some("terraphim"), + "webhook dispatch must resolve to terraphim-scoped reviewer" + ); +} diff --git a/crates/terraphim_orchestrator/tests/multi_project_tests.rs b/crates/terraphim_orchestrator/tests/multi_project_tests.rs new file mode 100644 index 000000000..df8aeb9ae --- /dev/null +++ b/crates/terraphim_orchestrator/tests/multi_project_tests.rs @@ -0,0 +1,359 @@ +//! Integration tests for multi-project config schema, include-glob loader, +//! and load-time validation (C1 banned providers, project references, +//! mixed-mode rejection). + +use std::path::PathBuf; + +use terraphim_orchestrator::config::OrchestratorConfig; +use terraphim_orchestrator::error::OrchestratorError; + +fn fixture(name: &str) -> PathBuf { + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests"); + p.push("fixtures"); + p.push("multi_project"); + p.push(name); + p +} + +#[test] +fn parses_inline_multi_project_config() { + let config = OrchestratorConfig::from_file(fixture("base_inline.toml")).unwrap(); + + assert_eq!(config.projects.len(), 2); + assert_eq!(config.projects[0].id, "alpha"); + assert_eq!(config.projects[1].id, "beta"); + assert_eq!(config.projects[1].schedule_offset_minutes, 5); + + assert_eq!(config.agents.len(), 2); + assert_eq!(config.agents[0].project.as_deref(), Some("alpha")); + assert_eq!(config.agents[1].project.as_deref(), Some("beta")); + + config.validate().unwrap(); +} + +#[test] +fn expands_include_glob_and_merges_fragments() { + let config = OrchestratorConfig::from_file(fixture("base_include.toml")).unwrap(); + + assert_eq!(config.projects.len(), 2); + let ids: Vec<&str> = config.projects.iter().map(|p| p.id.as_str()).collect(); + assert!(ids.contains(&"alpha")); + assert!(ids.contains(&"beta")); + + assert_eq!(config.agents.len(), 3); + let names: Vec<&str> = config.agents.iter().map(|a| a.name.as_str()).collect(); + assert!(names.contains(&"alpha-watcher")); + assert!(names.contains(&"beta-watcher")); + assert!(names.contains(&"beta-reviewer")); + + assert_eq!(config.flows.len(), 1); + assert_eq!(config.flows[0].name, "alpha-flow"); + assert_eq!(config.flows[0].project, "alpha"); + + assert_eq!(config.include, vec!["conf.d/*.toml".to_string()]); + + config.validate().unwrap(); +} + +#[test] +fn rejects_agent_with_unknown_project() { + let toml_str = r#" +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[agents]] +name = "ghost" +layer = "Safety" +cli_tool = "claude" +task = "Haunt" +project = "nonexistent" +"#; + let config = OrchestratorConfig::from_toml(toml_str).unwrap(); + let err = config.validate().unwrap_err(); + match err { + OrchestratorError::UnknownAgentProject { agent, project } => { + assert_eq!(agent, "ghost"); + assert_eq!(project, "nonexistent"); + } + other => panic!("expected UnknownAgentProject, got {other:?}"), + } +} + +#[test] +fn rejects_flow_with_unknown_project() { + let toml_str = r#" +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[agents]] +name = "alpha-watcher" +layer = "Safety" +cli_tool = "claude" +task = "Watch" +project = "alpha" + +[[flows]] +name = "orphan-flow" +project = "nonexistent" +repo_path = "/tmp/x" + +[[flows.steps]] +name = "build" +kind = "action" +command = "cargo build" +"#; + let config = OrchestratorConfig::from_toml(toml_str).unwrap(); + let err = config.validate().unwrap_err(); + match err { + OrchestratorError::UnknownFlowProject { flow, project } => { + assert_eq!(flow, "orphan-flow"); + assert_eq!(project, "nonexistent"); + } + other => panic!("expected UnknownFlowProject, got {other:?}"), + } +} + +#[test] +fn rejects_duplicate_project_id() { + let toml_str = r#" +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha2" + +[[agents]] +name = "a" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "alpha" +"#; + let config = OrchestratorConfig::from_toml(toml_str).unwrap(); + let err = config.validate().unwrap_err(); + match err { + OrchestratorError::DuplicateProjectId(id) => assert_eq!(id, "alpha"), + other => panic!("expected DuplicateProjectId, got {other:?}"), + } +} + +#[test] +fn rejects_mixed_mode_agent_without_project() { + let toml_str = r#" +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[agents]] +name = "with-project" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "alpha" + +[[agents]] +name = "without-project" +layer = "Safety" +cli_tool = "claude" +task = "t" +"#; + let config = OrchestratorConfig::from_toml(toml_str).unwrap(); + let err = config.validate().unwrap_err(); + match err { + OrchestratorError::MixedProjectMode { kind, name } => { + assert_eq!(kind, "agent"); + assert_eq!(name, "without-project"); + } + other => panic!("expected MixedProjectMode, got {other:?}"), + } +} + +#[test] +fn rejects_banned_provider_prefixes() { + let banned = [ + "opencode/foo", + "github-copilot/gpt-4", + "google/gemini-2", + "huggingface/llama3", + "minimax/abab", + ]; + for model in banned { + let toml_str = format!( + r#" +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "p" +working_dir = "/tmp/p" + +[[agents]] +name = "a" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "p" +model = "{model}" +"# + ); + let config = OrchestratorConfig::from_toml(&toml_str).unwrap(); + let err = config + .validate() + .err() + .unwrap_or_else(|| panic!("expected error for {model}")); + match err { + OrchestratorError::BannedProvider { + provider, field, .. + } => { + assert_eq!(provider, model, "provider mismatch for {model}"); + assert_eq!(field, "model"); + } + other => panic!("expected BannedProvider for {model}, got {other:?}"), + } + } +} + +#[test] +fn rejects_banned_fallback_provider() { + let toml_str = r#" +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "p" +working_dir = "/tmp/p" + +[[agents]] +name = "a" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "p" +model = "sonnet" +fallback_model = "google/gemini-2" +"#; + let config = OrchestratorConfig::from_toml(toml_str).unwrap(); + let err = config.validate().unwrap_err(); + match err { + OrchestratorError::BannedProvider { + field, provider, .. + } => { + assert_eq!(field, "fallback_model"); + assert_eq!(provider, "google/gemini-2"); + } + other => panic!("expected BannedProvider, got {other:?}"), + } +} + +#[test] +fn accepts_allowed_provider_prefixes_and_bare_models() { + let allowed = [ + "opencode-go/minimax-m2.5", + "kimi-for-coding/k2p5", + "minimax-coding-plan/abab", + "zai-coding-plan/glm-4", + "claude-code/sonnet", + "sonnet", + "opus", + "haiku", + ]; + for model in allowed { + let toml_str = format!( + r#" +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "p" +working_dir = "/tmp/p" + +[[agents]] +name = "a" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "p" +model = "{model}" +"# + ); + let config = OrchestratorConfig::from_toml(&toml_str).unwrap(); + config + .validate() + .unwrap_or_else(|e| panic!("model {model} should be allowed but got {e:?}")); + } +} + +#[test] +fn legacy_single_project_mode_parses_without_projects() { + let toml_str = r#" +working_dir = "/tmp/t" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[agents]] +name = "legacy-agent" +layer = "Safety" +cli_tool = "claude" +task = "t" +"#; + let config = OrchestratorConfig::from_toml(toml_str).unwrap(); + assert!(config.projects.is_empty()); + assert!(config.agents[0].project.is_none()); + config.validate().unwrap(); +} diff --git a/crates/terraphim_orchestrator/tests/orchestrator_tests.rs b/crates/terraphim_orchestrator/tests/orchestrator_tests.rs index e4449f090..6349d5ee4 100644 --- a/crates/terraphim_orchestrator/tests/orchestrator_tests.rs +++ b/crates/terraphim_orchestrator/tests/orchestrator_tests.rs @@ -80,6 +80,8 @@ fn test_config() -> OrchestratorConfig { pre_check: None, gitea_issue: None, + + project: None, }, AgentDefinition { name: "sync".to_string(), @@ -103,6 +105,8 @@ fn test_config() -> OrchestratorConfig { pre_check: None, gitea_issue: None, + + project: None, }, AgentDefinition { name: "reviewer".to_string(), @@ -126,6 +130,8 @@ fn test_config() -> OrchestratorConfig { pre_check: None, gitea_issue: None, + + project: None, }, ], restart_cooldown_secs: 60, @@ -143,6 +149,16 @@ fn test_config() -> OrchestratorConfig { webhook: None, role_config_path: None, routing: None, + #[cfg(feature = "quickwit")] + quickwit: None, + projects: vec![], + include: vec![], + providers: vec![], + provider_budget_state_file: None, + pause_dir: None, + project_circuit_breaker_threshold: 3, + fleet_escalation_owner: None, + fleet_escalation_repo: None, } } @@ -318,7 +334,7 @@ fn test_example_config_creates_orchestrator() { std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("orchestrator.example.toml"); let config = OrchestratorConfig::from_file(&example_path).unwrap(); - assert_eq!(config.agents.len(), 16); + assert_eq!(config.agents.len(), 18); assert_eq!(config.agents[0].layer, AgentLayer::Safety); assert_eq!(config.agents[1].layer, AgentLayer::Safety); assert_eq!(config.agents[2].layer, AgentLayer::Core); diff --git a/crates/terraphim_orchestrator/tests/pause_and_breaker_tests.rs b/crates/terraphim_orchestrator/tests/pause_and_breaker_tests.rs new file mode 100644 index 000000000..392cecb83 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/pause_and_breaker_tests.rs @@ -0,0 +1,268 @@ +//! Integration tests for the project pause flag gate and the project-meta +//! circuit breaker (Refs terraphim/adf-fleet#8). +//! +//! These tests avoid spawning real agent processes: pause-flag semantics live +//! entirely in filesystem state (`/`), and the breaker +//! is driven through `simulate_project_meta_failures_for_test`. The real +//! `spawn_agent` path is exercised via the public `spawn_agent_for_test` +//! helper which runs the pause gate and returns early when the flag is +//! present. + +use std::path::PathBuf; + +use tempfile::TempDir; +use terraphim_orchestrator::project_control; +use terraphim_orchestrator::{ + AgentDefinition, AgentLayer, AgentOrchestrator, CompoundReviewConfig, NightwatchConfig, + OrchestratorConfig, +}; + +fn project_agent(name: &str, project: Option<&str>) -> AgentDefinition { + AgentDefinition { + name: name.to_string(), + layer: AgentLayer::Core, + cli_tool: "/bin/true".to_string(), + task: String::new(), + schedule: None, + model: None, + capabilities: Vec::new(), + max_memory_bytes: None, + budget_monthly_cents: None, + provider: None, + persona: None, + terraphim_role: None, + skill_chain: Vec::new(), + sfia_skills: Vec::new(), + fallback_provider: None, + fallback_model: None, + grace_period_secs: None, + max_cpu_seconds: None, + pre_check: None, + gitea_issue: None, + project: project.map(|s| s.to_string()), + } +} + +fn test_config_with_pause(pause_dir: PathBuf, threshold: u32) -> OrchestratorConfig { + OrchestratorConfig { + working_dir: PathBuf::from("/tmp/test-orchestrator-breaker"), + nightwatch: NightwatchConfig::default(), + compound_review: CompoundReviewConfig { + cli_tool: None, + provider: None, + model: None, + schedule: "0 2 * * *".to_string(), + max_duration_secs: 60, + repo_path: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."), + create_prs: false, + worktree_root: PathBuf::from("/tmp/test-orchestrator-breaker/.worktrees"), + base_branch: "main".to_string(), + max_concurrent_agents: 3, + ..Default::default() + }, + workflow: None, + agents: vec![ + project_agent("odilo-worker", Some("odilo")), + project_agent("legacy-worker", None), + project_agent("digital-twins-worker", Some("digital-twins")), + ], + restart_cooldown_secs: 60, + max_restart_count: 10, + restart_budget_window_secs: 43_200, + disk_usage_threshold: 100, + tick_interval_secs: 30, + handoff_buffer_ttl_secs: None, + persona_data_dir: None, + skill_data_dir: None, + flows: vec![], + flow_state_dir: None, + gitea: None, + mentions: None, + webhook: None, + role_config_path: None, + routing: None, + #[cfg(feature = "quickwit")] + quickwit: None, + projects: vec![], + include: vec![], + providers: vec![], + provider_budget_state_file: None, + pause_dir: Some(pause_dir), + project_circuit_breaker_threshold: threshold, + fleet_escalation_owner: None, + fleet_escalation_repo: None, + } +} + +#[tokio::test] +async fn pause_flag_blocks_dispatch_for_matching_project() { + let tmp = TempDir::new().unwrap(); + let config = test_config_with_pause(tmp.path().to_path_buf(), 3); + let mut orch = AgentOrchestrator::new(config).unwrap(); + + // Pre-create the pause flag for "odilo". + project_control::touch_pause_flag(tmp.path(), "odilo").unwrap(); + + // spawn_agent_for_test runs the full spawn_agent gate. A paused project + // returns Ok(()) without creating an active agent record. + orch.spawn_agent_for_test("odilo-worker") + .await + .expect("paused spawn should return Ok(()) without erroring"); + + assert!( + !orch.is_agent_active("odilo-worker"), + "paused project must not produce an active agent record" + ); +} + +#[tokio::test] +async fn absent_pause_flag_does_not_block_dispatch() { + let tmp = TempDir::new().unwrap(); + let config = test_config_with_pause(tmp.path().to_path_buf(), 3); + let orch = AgentOrchestrator::new(config).unwrap(); + + // No pause flag: the spawn path runs to completion. The underlying + // spawner may still fail on `/bin/true` for unrelated reasons; what we + // assert is that the *pause gate* did not short-circuit, which is + // reflected by the orchestrator having at least attempted the spawn. + // We observe this indirectly: if the pause gate had fired, the active + // agent map would be untouched AND no error would have been returned. + // Since we cannot distinguish those two states from "paused" without + // extra hooks, we instead verify the invariant that without the flag + // the pause check itself reports not-paused. + assert!( + !project_control::is_project_paused(orch.pause_dir_for_test(), Some("odilo")), + "no pause flag should mean not paused" + ); +} + +#[tokio::test] +async fn pause_flag_is_project_scoped() { + let tmp = TempDir::new().unwrap(); + let config = test_config_with_pause(tmp.path().to_path_buf(), 3); + let mut orch = AgentOrchestrator::new(config).unwrap(); + + project_control::touch_pause_flag(tmp.path(), "odilo").unwrap(); + + // odilo is paused; digital-twins is not. + assert!(project_control::is_project_paused( + orch.pause_dir_for_test(), + Some("odilo") + )); + assert!(!project_control::is_project_paused( + orch.pause_dir_for_test(), + Some("digital-twins") + )); + + // The dispatch for odilo should be gated. + orch.spawn_agent_for_test("odilo-worker").await.unwrap(); + assert!(!orch.is_agent_active("odilo-worker")); +} + +#[tokio::test] +async fn pause_flag_does_not_affect_legacy_global_agents() { + let tmp = TempDir::new().unwrap(); + let config = test_config_with_pause(tmp.path().to_path_buf(), 3); + let _orch = AgentOrchestrator::new(config).unwrap(); + + // Even with an exotic "__global__" pause flag, legacy (project=None) + // agents must never be blocked by the project pause mechanism. + project_control::touch_pause_flag(tmp.path(), "__global__").unwrap(); + assert!(!project_control::is_project_paused(tmp.path(), None)); +} + +#[tokio::test] +async fn circuit_breaker_trips_at_threshold_and_touches_pause_flag() { + let tmp = TempDir::new().unwrap(); + let config = test_config_with_pause(tmp.path().to_path_buf(), 3); + let mut orch = AgentOrchestrator::new(config).unwrap(); + + // Two failures under threshold: no pause flag yet. + let tripped_two = orch + .simulate_project_meta_failures_for_test("odilo", 2) + .await; + assert!(!tripped_two, "threshold=3, two failures must not trip"); + assert!(!project_control::is_project_paused( + orch.pause_dir_for_test(), + Some("odilo") + )); + + // Third failure: threshold reached, pause flag created. + let tripped_three = orch + .simulate_project_meta_failures_for_test("odilo", 1) + .await; + assert!(tripped_three, "threshold=3, third failure must trip"); + assert!(project_control::is_project_paused( + orch.pause_dir_for_test(), + Some("odilo") + )); +} + +#[tokio::test] +async fn circuit_breaker_resets_on_success() { + let tmp = TempDir::new().unwrap(); + let config = test_config_with_pause(tmp.path().to_path_buf(), 3); + let mut orch = AgentOrchestrator::new(config).unwrap(); + + orch.simulate_project_meta_failures_for_test("odilo", 2) + .await; + + // Success resets the streak; the next two failures should NOT trip. + orch.reset_project_meta_counter_for_test("odilo"); + let tripped = orch + .simulate_project_meta_failures_for_test("odilo", 2) + .await; + + assert!( + !tripped, + "counter must reset on success; two further failures under threshold=3 must not trip" + ); + assert!( + !project_control::is_project_paused(orch.pause_dir_for_test(), Some("odilo")), + "no pause flag should exist after reset + 2 failures" + ); +} + +#[tokio::test] +async fn circuit_breaker_is_per_project() { + let tmp = TempDir::new().unwrap(); + let config = test_config_with_pause(tmp.path().to_path_buf(), 2); + let mut orch = AgentOrchestrator::new(config).unwrap(); + + // Trip odilo. + let odilo_tripped = orch + .simulate_project_meta_failures_for_test("odilo", 2) + .await; + assert!(odilo_tripped); + + // digital-twins must still be clean. + assert!(project_control::is_project_paused( + orch.pause_dir_for_test(), + Some("odilo") + )); + assert!(!project_control::is_project_paused( + orch.pause_dir_for_test(), + Some("digital-twins") + )); +} + +#[tokio::test] +async fn pause_flag_removal_restores_dispatch() { + let tmp = TempDir::new().unwrap(); + let config = test_config_with_pause(tmp.path().to_path_buf(), 3); + let orch = AgentOrchestrator::new(config).unwrap(); + + // Create the flag, observe paused, remove, observe not-paused. + project_control::touch_pause_flag(tmp.path(), "odilo").unwrap(); + assert!(project_control::is_project_paused( + orch.pause_dir_for_test(), + Some("odilo") + )); + + std::fs::remove_file(tmp.path().join("odilo")).unwrap(); + + assert!( + !project_control::is_project_paused(orch.pause_dir_for_test(), Some("odilo")), + "after removal the pause gate must no longer block" + ); +} diff --git a/crates/terraphim_orchestrator/tests/project_runtime_tests.rs b/crates/terraphim_orchestrator/tests/project_runtime_tests.rs new file mode 100644 index 000000000..07795309d --- /dev/null +++ b/crates/terraphim_orchestrator/tests/project_runtime_tests.rs @@ -0,0 +1,212 @@ +//! Integration tests for runtime project plumbing (issue terraphim/adf-fleet#4): +//! OutputPoster per-project routing, legacy-mode fallback, and concurrency / +//! dispatcher fairness seen through public APIs. + +use terraphim_orchestrator::config::OrchestratorConfig; +use terraphim_orchestrator::dispatcher::{DispatchTask, Dispatcher, LEGACY_PROJECT_ID}; +use terraphim_orchestrator::output_poster::OutputPoster; + +fn two_project_config() -> OrchestratorConfig { + let toml_str = r#" +working_dir = "/tmp/adf" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[gitea] +base_url = "https://git.example.test" +token = "legacy-token" +owner = "legacy-owner" +repo = "legacy-repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[projects.gitea] +base_url = "https://git.example.test" +token = "alpha-token" +owner = "alpha-owner" +repo = "alpha-repo" + +[[projects]] +id = "beta" +working_dir = "/tmp/beta" + +[projects.gitea] +base_url = "https://git.example.test" +token = "beta-token" +owner = "beta-owner" +repo = "beta-repo" + +[[agents]] +name = "alpha-worker" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "alpha" + +[[agents]] +name = "beta-worker" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "beta" +"#; + OrchestratorConfig::from_toml(toml_str).unwrap() +} + +#[test] +fn output_poster_routes_per_project_and_to_legacy_fallback() { + let config = two_project_config(); + let poster = + OutputPoster::from_orchestrator_config(&config).expect("expected poster to be constructed"); + + // Agent lookups resolve to the correct project's tracker (owner/repo). + let alpha = poster + .tracker_for("alpha", "alpha-worker") + .expect("alpha tracker"); + assert_eq!(alpha.owner(), "alpha-owner"); + assert_eq!(alpha.repo(), "alpha-repo"); + + let beta = poster + .tracker_for("beta", "beta-worker") + .expect("beta tracker"); + assert_eq!(beta.owner(), "beta-owner"); + assert_eq!(beta.repo(), "beta-repo"); + + // Unknown project ids fall back to the legacy project (top-level gitea). + let legacy = poster + .tracker_for(LEGACY_PROJECT_ID, "alpha-worker") + .expect("legacy tracker"); + assert_eq!(legacy.owner(), "legacy-owner"); + assert_eq!(legacy.repo(), "legacy-repo"); + + let unknown = poster + .tracker_for("does-not-exist", "anybody") + .expect("fallback tracker"); + assert_eq!(unknown.owner(), "legacy-owner"); +} + +#[test] +fn output_poster_legacy_single_project_still_addressable() { + // Legacy single-project config: only top-level gitea, no [[projects]]. + let toml_str = r#" +working_dir = "/tmp/adf" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[gitea] +base_url = "https://git.example.test" +token = "legacy-token" +owner = "legacy-owner" +repo = "legacy-repo" + +[[agents]] +name = "legacy" +layer = "Safety" +cli_tool = "claude" +task = "t" +"#; + let config = OrchestratorConfig::from_toml(toml_str).unwrap(); + let poster = OutputPoster::from_orchestrator_config(&config).expect("legacy poster constructs"); + + // Legacy project id resolves the top-level tracker. + let tracker = poster + .tracker_for(LEGACY_PROJECT_ID, "legacy") + .expect("legacy tracker"); + assert_eq!(tracker.owner(), "legacy-owner"); + assert_eq!(tracker.repo(), "legacy-repo"); + + // Unknown project ids also fall back to legacy. + let fallback = poster + .tracker_for("unknown", "legacy") + .expect("fallback resolves to legacy"); + assert_eq!(fallback.owner(), "legacy-owner"); +} + +#[test] +fn output_poster_without_gitea_returns_none() { + let toml_str = r#" +working_dir = "/tmp/adf" + +[nightwatch] + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/repo" + +[[projects]] +id = "alpha" +working_dir = "/tmp/alpha" + +[[agents]] +name = "alpha-worker" +layer = "Safety" +cli_tool = "claude" +task = "t" +project = "alpha" +"#; + let config = OrchestratorConfig::from_toml(toml_str).unwrap(); + assert!(OutputPoster::from_orchestrator_config(&config).is_none()); +} + +#[test] +fn dispatcher_round_robin_fairness_across_projects() { + let mut dispatcher = Dispatcher::new(); + + // Enqueue three alpha tasks and one beta task at the same layer/score. + for name in ["a1", "a2", "a3"] { + dispatcher.enqueue(DispatchTask::TimeDriven { + name: name.into(), + task: "t".into(), + layer: terraphim_orchestrator::AgentLayer::Core, + project: "alpha".into(), + }); + } + dispatcher.enqueue(DispatchTask::TimeDriven { + name: "b1".into(), + task: "t".into(), + layer: terraphim_orchestrator::AgentLayer::Core, + project: "beta".into(), + }); + + // First dequeue: alpha wins on FIFO. + assert_eq!(dispatcher.dequeue().unwrap().project(), "alpha"); + // Second dequeue: beta jumps ahead via round-robin (never served yet). + assert_eq!(dispatcher.dequeue().unwrap().project(), "beta"); + // Remaining two are alpha-only. + assert_eq!(dispatcher.dequeue().unwrap().project(), "alpha"); + assert_eq!(dispatcher.dequeue().unwrap().project(), "alpha"); +} + +#[test] +fn dispatcher_by_project_stats_track_enqueue_dequeue() { + let mut dispatcher = Dispatcher::new(); + dispatcher.enqueue(DispatchTask::TimeDriven { + name: "a".into(), + task: "t".into(), + layer: terraphim_orchestrator::AgentLayer::Core, + project: "alpha".into(), + }); + dispatcher.enqueue(DispatchTask::TimeDriven { + name: "b".into(), + task: "t".into(), + layer: terraphim_orchestrator::AgentLayer::Core, + project: "beta".into(), + }); + assert_eq!(dispatcher.stats().by_project.get("alpha"), Some(&1)); + assert_eq!(dispatcher.stats().by_project.get("beta"), Some(&1)); + + dispatcher.dequeue(); + // Still one of the two remains. + let total_remaining: u64 = dispatcher.stats().by_project.values().sum(); + assert_eq!(total_remaining, 1); +} diff --git a/crates/terraphim_orchestrator/tests/provider_gate_tests.rs b/crates/terraphim_orchestrator/tests/provider_gate_tests.rs new file mode 100644 index 000000000..434d878ab --- /dev/null +++ b/crates/terraphim_orchestrator/tests/provider_gate_tests.rs @@ -0,0 +1,576 @@ +//! Integration tests for the subscription-aware provider gate and +//! per-provider hour/day budget tracker (Gitea #6). +//! +//! Scenarios exercised: +//! 1. C1/C3 allow-list drops a banned static candidate. +//! 2. `CostTracker::should_pause()` skips dispatch for an exhausted +//! monthly-budget agent. +//! 3. `ProviderBudgetTracker` hour window exhausts and recovers. +//! 4. Day window exhausts independently of the hour window. +//! 5. Reloading the tracker from a snapshot discards state for providers +//! that were removed from config. +//! 6. Persistence round-trip survives `with_persistence`. +//! 7. `RoutingDecisionEngine` drops `Exhausted` candidates before scoring. +//! +//! These tests hit real implementations -- no mocks. They avoid the +//! `#[cfg(test)]` module scoping of unit tests so they exercise the +//! public surface and catch any future visibility regressions. + +use chrono::{TimeZone, Utc}; +use std::path::PathBuf; +use std::sync::Arc; + +use terraphim_orchestrator::config::is_allowed_provider; +use terraphim_orchestrator::control_plane::routing::{ + BudgetPressure, DispatchContext, RouteSource, RoutingDecisionEngine, +}; +use terraphim_orchestrator::control_plane::telemetry::{CompletionEvent, TokenBreakdown}; +use terraphim_orchestrator::cost_tracker::{BudgetVerdict, CostTracker}; +use terraphim_orchestrator::provider_budget::{ + provider_has_budget, provider_key_for_model, ProviderBudgetConfig, ProviderBudgetTracker, +}; +use terraphim_orchestrator::{ + AgentDefinition, AgentLayer, AgentOrchestrator, CompoundReviewConfig, NightwatchConfig, + OrchestratorConfig, +}; + +fn dispatch_ctx_with_static(agent: &str, model: &str) -> DispatchContext { + DispatchContext { + agent_name: agent.to_string(), + task: "task body".to_string(), + static_model: Some(model.to_string()), + cli_tool: "opencode".to_string(), + layer: terraphim_orchestrator::config::AgentLayer::Core, + session_id: None, + } +} + +// === Scenario 1: C1/C3 allow-list ========================================== + +#[test] +fn c1_allowed_prefixes_pass() { + for allowed in [ + "claude-code/anthropic/claude-sonnet-4-5", + "opencode-go/minimax-m2.5", + "kimi-for-coding/k2p5", + "minimax-coding-plan/MiniMax-M2.5", + "zai-coding-plan/glm-4.6", + "sonnet", + "opus", + "haiku", + "anthropic/claude-3-5-sonnet", + ] { + assert!( + is_allowed_provider(allowed), + "expected {allowed} to pass allow-list" + ); + } +} + +#[test] +fn c3_banned_prefixes_rejected() { + for banned in [ + "opencode/gpt-4", + "github-copilot/gpt-5", + "google/gemini-2.0", + "huggingface/some-model", + "minimax/MiniMax-M2.5", + ] { + assert!( + !is_allowed_provider(banned), + "expected {banned} to be banned" + ); + } +} + +// === Scenario 2: CostTracker should_pause dispatch skip =================== + +#[test] +fn cost_tracker_should_pause_reports_exhausted() { + let mut ct = CostTracker::new(); + ct.register("cold-agent", Some(100)); // $1 cap + ct.record_cost("cold-agent", 2.00); // $2 spent + let verdict = ct.check("cold-agent"); + assert!( + verdict.should_pause(), + "expected should_pause() true, got {verdict:?}" + ); + assert!(matches!(verdict, BudgetVerdict::Exhausted { .. })); +} + +#[test] +fn cost_tracker_uncapped_never_pauses() { + let mut ct = CostTracker::new(); + ct.register("unbounded", None); + ct.record_cost("unbounded", 9999.0); + let verdict = ct.check("unbounded"); + assert!(!verdict.should_pause()); + assert!(matches!(verdict, BudgetVerdict::Uncapped)); +} + +// === Scenario 3: Hour window exhausts and recovers ======================== + +#[test] +fn hour_window_exhausts_and_recovers_next_hour() { + let t = ProviderBudgetTracker::new(vec![ProviderBudgetConfig { + id: "opencode-go".to_string(), + max_hour_cents: Some(100), + max_day_cents: None, + error_signatures: None, + }]); + let t0 = Utc.with_ymd_and_hms(2026, 4, 19, 10, 30, 0).unwrap(); + let t_next = Utc.with_ymd_and_hms(2026, 4, 19, 11, 5, 0).unwrap(); + + let _ = t.record_cost_at("opencode-go", 1.50, t0); + assert!(matches!( + t.check_at("opencode-go", t0), + BudgetVerdict::Exhausted { .. } + )); + // Next hour -> fresh bucket. + assert_eq!( + t.check_at("opencode-go", t_next), + BudgetVerdict::WithinBudget + ); +} + +// === Scenario 4: Day window independent of hour =========================== + +#[test] +fn day_window_independent_of_hour() { + let t = ProviderBudgetTracker::new(vec![ProviderBudgetConfig { + id: "opencode-go".to_string(), + max_hour_cents: Some(100), + max_day_cents: Some(150), + error_signatures: None, + }]); + let t0 = Utc.with_ymd_and_hms(2026, 4, 19, 10, 0, 0).unwrap(); + let t1 = Utc.with_ymd_and_hms(2026, 4, 19, 11, 0, 0).unwrap(); + + // $0.90 in hour 10 -> both windows: near but not exhausted. + let _ = t.record_cost_at("opencode-go", 0.90, t0); + // $0.70 in hour 11 -> hour bucket is only $0.70 (healthy) but day + // bucket is now $1.60 > $1.50 cap -> Exhausted. + let _ = t.record_cost_at("opencode-go", 0.70, t1); + let verdict = t.check_at("opencode-go", t1); + assert!( + matches!(verdict, BudgetVerdict::Exhausted { .. }), + "day cap should trip across hour boundary; got {verdict:?}" + ); +} + +// === Scenario 5: stale snapshot entries discarded ========================= + +#[test] +fn reload_drops_state_for_removed_providers() { + let tmp = tempfile::NamedTempFile::new().unwrap(); + let path = tmp.path().to_path_buf(); + // Drop the empty placeholder so `with_persistence` treats it as missing. + drop(tmp); + + // Session 1: persist state for "old-provider". + let t1 = ProviderBudgetTracker::with_persistence( + vec![ProviderBudgetConfig { + id: "old-provider".to_string(), + max_hour_cents: Some(100), + max_day_cents: None, + error_signatures: None, + }], + path.clone(), + ) + .unwrap(); + let now = Utc.with_ymd_and_hms(2026, 4, 19, 10, 0, 0).unwrap(); + let _ = t1.record_cost_at("old-provider", 0.50, now); + t1.persist().unwrap(); + + // Session 2: config removes "old-provider", adds "new-provider". + let t2 = ProviderBudgetTracker::with_persistence( + vec![ProviderBudgetConfig { + id: "new-provider".to_string(), + max_hour_cents: Some(100), + max_day_cents: None, + error_signatures: None, + }], + path.clone(), + ) + .unwrap(); + let snap = t2.snapshot(); + assert!( + !snap.providers.contains_key("old-provider"), + "stale provider state must not leak across config edits" + ); + + let _ = std::fs::remove_file(&path); +} + +// === Scenario 6: persistence round-trip =================================== + +#[test] +fn persistence_round_trip_preserves_spend() { + let tmp = tempfile::NamedTempFile::new().unwrap(); + let path = tmp.path().to_path_buf(); + drop(tmp); + + let cfgs = vec![ProviderBudgetConfig { + id: "kimi-for-coding".to_string(), + max_hour_cents: Some(500), + max_day_cents: Some(2000), + error_signatures: None, + }]; + + let t1 = ProviderBudgetTracker::with_persistence(cfgs.clone(), path.clone()).unwrap(); + let t0 = Utc.with_ymd_and_hms(2026, 4, 19, 10, 0, 0).unwrap(); + let _ = t1.record_cost_at("kimi-for-coding", 1.23, t0); + t1.persist().unwrap(); + + let t2 = ProviderBudgetTracker::with_persistence(cfgs, path.clone()).unwrap(); + let snap = t2.snapshot(); + let entry = snap + .providers + .get("kimi-for-coding") + .expect("provider state must survive round-trip"); + // $1.23 -> 12_300 sub-cents (hundredths). + assert_eq!(entry.hour.sub_cents, 12_300); + assert_eq!(entry.day.sub_cents, 12_300); + + let _ = std::fs::remove_file(&path); +} + +// === Scenario 7: routing drops Exhausted candidate ======================== + +#[tokio::test] +async fn routing_drops_provider_budget_exhausted_candidate() { + // opencode-go: $0.50/hour. Pre-spend $1.00 to exhaust the hour + // bucket, then ask the routing engine to pick a candidate whose + // model prefix is opencode-go. It must be filtered out and the + // engine must fall back to the CLI default. + let tracker = ProviderBudgetTracker::new(vec![ProviderBudgetConfig { + id: "opencode-go".to_string(), + max_hour_cents: Some(50), + max_day_cents: None, + error_signatures: None, + }]); + let _ = tracker.record_cost("opencode-go", 1.00); + assert!( + !provider_has_budget(&tracker, "opencode-go"), + "sanity: provider should be exhausted before the routing call" + ); + + let engine = RoutingDecisionEngine::with_provider_budget( + None, + Vec::new(), + terraphim_router::Router::new(), + None, + Some(Arc::new(tracker)), + ); + + let ctx = dispatch_ctx_with_static("agent", "opencode-go/minimax-m2.5"); + let decision = engine.decide_route(&ctx, &BudgetVerdict::Uncapped).await; + + assert_eq!( + decision.candidate.source, + RouteSource::CliDefault, + "exhausted candidate must not win; rationale={}", + decision.rationale + ); + assert!( + decision.rationale.contains("provider-budget"), + "rationale should reference provider-budget: {}", + decision.rationale + ); + assert_eq!(decision.budget_pressure, BudgetPressure::NoPressure); +} + +// === Scenario 8: record_telemetry wiring drives ProviderBudgetTracker ==== + +fn agent_with_model(name: &str, model: &str) -> AgentDefinition { + AgentDefinition { + name: name.to_string(), + layer: AgentLayer::Core, + cli_tool: "echo".to_string(), + task: "task".to_string(), + model: Some(model.to_string()), + schedule: None, + capabilities: vec![], + max_memory_bytes: None, + budget_monthly_cents: Some(10_000), + provider: None, + persona: None, + terraphim_role: None, + skill_chain: vec![], + sfia_skills: vec![], + fallback_provider: None, + fallback_model: None, + grace_period_secs: None, + max_cpu_seconds: None, + pre_check: None, + gitea_issue: None, + project: None, + } +} + +fn budget_aware_config( + providers: Vec, + state_file: Option, + agents: Vec, +) -> OrchestratorConfig { + OrchestratorConfig { + working_dir: PathBuf::from("/tmp/terraphim-provider-budget-test"), + nightwatch: NightwatchConfig::default(), + compound_review: CompoundReviewConfig { + cli_tool: None, + provider: None, + model: None, + schedule: "0 2 * * *".to_string(), + max_duration_secs: 60, + repo_path: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."), + create_prs: false, + worktree_root: PathBuf::from("/tmp/terraphim-provider-budget-test/.worktrees"), + base_branch: "main".to_string(), + max_concurrent_agents: 3, + ..Default::default() + }, + workflow: None, + agents, + restart_cooldown_secs: 60, + max_restart_count: 10, + restart_budget_window_secs: 43_200, + disk_usage_threshold: 100, + tick_interval_secs: 30, + handoff_buffer_ttl_secs: None, + persona_data_dir: None, + skill_data_dir: None, + flows: vec![], + flow_state_dir: None, + gitea: None, + mentions: None, + webhook: None, + role_config_path: None, + routing: None, + #[cfg(feature = "quickwit")] + quickwit: None, + projects: vec![], + include: vec![], + providers, + provider_budget_state_file: state_file, + pause_dir: None, + project_circuit_breaker_threshold: 3, + fleet_escalation_owner: None, + fleet_escalation_repo: None, + } +} + +fn completion_event(model: &str, cost_usd: f64) -> CompletionEvent { + CompletionEvent { + model: model.to_string(), + session_id: "sess-1".to_string(), + completed_at: Utc::now(), + latency_ms: 100, + success: true, + tokens: TokenBreakdown::default(), + cost_usd, + error: None, + } +} + +#[tokio::test] +async fn record_telemetry_feeds_provider_budget_tracker() { + // Regression for the P1 "Layer 3 is read-only at runtime": feed + // telemetry events through the orchestrator's `record_telemetry` + // and confirm the provider budget tracker observes the spend. + let providers = vec![ProviderBudgetConfig { + id: "opencode-go".to_string(), + max_hour_cents: Some(50), + max_day_cents: Some(200), + error_signatures: None, + }]; + let config = budget_aware_config( + providers, + None, + vec![agent_with_model("worker", "opencode-go/minimax-m2.5")], + ); + + let orch = AgentOrchestrator::new(config).expect("build orchestrator"); + let tracker = orch + .provider_budget_tracker() + .cloned() + .expect("tracker must be constructed when [[providers]] is set"); + + // Sanity: nothing recorded yet, budget is healthy. + assert_eq!(tracker.check("opencode-go"), BudgetVerdict::WithinBudget); + + // Feed a real CompletionEvent: $0.40 to opencode-go. Below the $0.50 + // cap so verdict stays WithinBudget but `sub_cents` must update. + orch.record_telemetry_for_test(vec![( + "worker".to_string(), + completion_event("opencode-go/minimax-m2.5", 0.40), + )]) + .await; + + let snap_1 = tracker.snapshot(); + let entry_1 = snap_1 + .providers + .get("opencode-go") + .expect("provider state must exist after telemetry wire-up"); + assert_eq!( + entry_1.hour.sub_cents, 4_000, + "record_telemetry must route $0.40 into the hour bucket" + ); + assert_eq!(entry_1.day.sub_cents, 4_000); + + // Feed another event that should push the hour cap over; the + // routing-side `check()` must now observe Exhausted. + orch.record_telemetry_for_test(vec![( + "worker".to_string(), + completion_event("opencode-go/minimax-m2.5", 0.30), + )]) + .await; + + assert!( + matches!( + tracker.check("opencode-go"), + BudgetVerdict::Exhausted { .. } + ), + "second record must tip the hour cap to Exhausted; snapshot={:?}", + tracker.snapshot() + ); +} + +#[tokio::test] +async fn record_telemetry_ignores_zero_cost_and_unknown_model() { + // zero-cost events must be a no-op; unknown bare models silently + // feed their own synthetic key (rather than panicking or mis-routing). + let providers = vec![ProviderBudgetConfig { + id: "kimi-for-coding".to_string(), + max_hour_cents: Some(100), + max_day_cents: None, + error_signatures: None, + }]; + let config = budget_aware_config( + providers, + None, + vec![agent_with_model("worker", "kimi-for-coding/k2p5")], + ); + let orch = AgentOrchestrator::new(config).expect("build orchestrator"); + let tracker = orch + .provider_budget_tracker() + .cloned() + .expect("tracker must be constructed"); + + // Zero cost event -- no spend recorded. + orch.record_telemetry_for_test(vec![( + "worker".to_string(), + completion_event("kimi-for-coding/k2p5", 0.0), + )]) + .await; + assert_eq!( + tracker + .snapshot() + .providers + .get("kimi-for-coding") + .map(|e| e.hour.sub_cents) + .unwrap_or(0), + 0 + ); + + // Model whose provider is not in [[providers]] -- should not + // poison the tracker state for providers we *do* track. + orch.record_telemetry_for_test(vec![( + "worker".to_string(), + completion_event("opencode-go/minimax-m2.5", 0.25), + )]) + .await; + assert_eq!( + tracker + .snapshot() + .providers + .get("kimi-for-coding") + .map(|e| e.hour.sub_cents) + .unwrap_or(0), + 0, + "unrelated provider spend must not land in kimi-for-coding's bucket" + ); +} + +// === Scenario 9: persistence across a simulated restart ================== + +#[tokio::test] +async fn provider_budget_persistence_round_trip_via_orchestrator() { + // Drive the tracker through the orchestrator, persist, then build + // a new orchestrator from the same config + state file and verify + // the counters carry across the "restart". + let tmp = tempfile::NamedTempFile::new().unwrap(); + let state_path = tmp.path().to_path_buf(); + drop(tmp); + + let providers = vec![ProviderBudgetConfig { + id: "opencode-go".to_string(), + max_hour_cents: Some(500), + max_day_cents: Some(2_000), + error_signatures: None, + }]; + + // Session 1: spend + persist. + { + let config = budget_aware_config( + providers.clone(), + Some(state_path.clone()), + vec![agent_with_model("worker", "opencode-go/minimax-m2.5")], + ); + let orch = AgentOrchestrator::new(config).expect("build orchestrator"); + orch.record_telemetry_for_test(vec![( + "worker".to_string(), + completion_event("opencode-go/minimax-m2.5", 1.23), + )]) + .await; + // Explicit persist to mimic the reconcile-tick flush. + orch.provider_budget_tracker() + .expect("tracker") + .persist() + .expect("persist must succeed"); + } + + // Session 2: rebuild from config + state; counters must survive. + let config2 = budget_aware_config( + providers, + Some(state_path.clone()), + vec![agent_with_model("worker", "opencode-go/minimax-m2.5")], + ); + let orch2 = AgentOrchestrator::new(config2).expect("rebuild orchestrator"); + let snap = orch2 + .provider_budget_tracker() + .expect("tracker present on restart") + .snapshot(); + let entry = snap + .providers + .get("opencode-go") + .expect("state must be reloaded"); + assert_eq!( + entry.hour.sub_cents, 12_300, + "hour bucket must survive restart" + ); + assert_eq!( + entry.day.sub_cents, 12_300, + "day bucket must survive restart" + ); + + let _ = std::fs::remove_file(&state_path); +} + +// === Helper: provider_key_for_model edges ================================= + +#[test] +fn provider_key_helper_classifies_bare_and_prefixed() { + assert_eq!( + provider_key_for_model("opencode-go/minimax-m2.5"), + Some("opencode-go") + ); + assert_eq!( + provider_key_for_model("kimi-for-coding/k2p5"), + Some("kimi-for-coding") + ); + assert_eq!(provider_key_for_model("sonnet"), Some("claude-code")); + assert_eq!(provider_key_for_model("opus"), Some("claude-code")); + assert_eq!(provider_key_for_model("anthropic"), Some("claude-code")); + // Unknown bare identifier -> echoed back as its own key. + assert_eq!(provider_key_for_model("mystery"), Some("mystery")); +} diff --git a/crates/terraphim_orchestrator/tests/runtime_validate_tests.rs b/crates/terraphim_orchestrator/tests/runtime_validate_tests.rs new file mode 100644 index 000000000..9840f6ca2 --- /dev/null +++ b/crates/terraphim_orchestrator/tests/runtime_validate_tests.rs @@ -0,0 +1,147 @@ +//! Constructor-level validation tests for `AgentOrchestrator::from_config_file`. +//! +//! These verify that invalid configs are rejected at production startup, +//! not just in the `adf --check` dry-run path. + +use std::path::PathBuf; + +use terraphim_orchestrator::error::OrchestratorError; +use terraphim_orchestrator::AgentOrchestrator; + +fn fixture(name: &str) -> PathBuf { + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.push("tests"); + p.push("fixtures"); + p.push("runtime_validate"); + p.push(name); + p +} + +#[test] +fn rejects_banned_provider_at_startup() { + let result = AgentOrchestrator::from_config_file(fixture("banned_provider.toml")); + assert!(result.is_err(), "expected Err for banned provider"); + let err = result.err().unwrap(); + match err { + OrchestratorError::BannedProvider { + agent, + provider, + field, + } => { + assert_eq!(agent, "bad-agent"); + assert!( + provider.starts_with("opencode/"), + "expected opencode/ prefix, got {provider}" + ); + assert_eq!(field, "model"); + } + other => panic!("expected BannedProvider, got {other:?}"), + } +} + +#[test] +fn rejects_unknown_project_ref_at_startup() { + let result = AgentOrchestrator::from_config_file(fixture("unknown_project_ref.toml")); + assert!(result.is_err(), "expected Err for unknown project ref"); + let err = result.err().unwrap(); + match err { + OrchestratorError::UnknownAgentProject { agent, project } => { + assert_eq!(agent, "ghost-agent"); + assert_eq!(project, "nonexistent"); + } + other => panic!("expected UnknownAgentProject, got {other:?}"), + } +} + +#[test] +fn rejects_duplicate_project_id_at_startup() { + let result = AgentOrchestrator::from_config_file(fixture("duplicate_project_id.toml")); + assert!(result.is_err(), "expected Err for duplicate project id"); + let err = result.err().unwrap(); + match err { + OrchestratorError::DuplicateProjectId(id) => { + assert_eq!(id, "alpha"); + } + other => panic!("expected DuplicateProjectId, got {other:?}"), + } +} + +#[test] +fn rejects_mixed_mode_at_startup() { + let result = AgentOrchestrator::from_config_file(fixture("mixed_mode.toml")); + assert!(result.is_err(), "expected Err for mixed project mode"); + let err = result.err().unwrap(); + match err { + OrchestratorError::MixedProjectMode { kind, name } => { + assert_eq!(kind, "agent"); + assert_eq!(name, "unbound-agent"); + } + other => panic!("expected MixedProjectMode, got {other:?}"), + } +} + +#[test] +fn rejects_banned_fallback_model_at_startup() { + let result = AgentOrchestrator::from_config_file(fixture("banned_fallback.toml")); + assert!(result.is_err(), "expected Err for banned fallback_model"); + let err = result.err().unwrap(); + match err { + OrchestratorError::BannedProvider { + agent, + provider, + field, + } => { + assert_eq!(agent, "bad-fallback-agent"); + assert!( + provider.starts_with("opencode/"), + "expected opencode/ prefix, got {provider}" + ); + assert_eq!(field, "fallback_model"); + } + other => panic!("expected BannedProvider on fallback_model, got {other:?}"), + } +} + +#[test] +fn rejects_mixed_mode_flow_at_startup() { + let result = AgentOrchestrator::from_config_file(fixture("mixed_mode_flow.toml")); + assert!( + result.is_err(), + "expected Err for flow in legacy (no-projects) mode" + ); + let err = result.err().unwrap(); + match err { + OrchestratorError::MixedProjectMode { kind, name } => { + assert_eq!(kind, "flow"); + assert_eq!(name, "orphan-flow"); + } + other => panic!("expected MixedProjectMode for flow, got {other:?}"), + } +} + +#[test] +fn accepts_valid_multi_project_config_at_startup() { + let orch = AgentOrchestrator::from_config_file(fixture("valid_multi_project.toml")) + .expect("valid multi-project config should load"); + + let cfg = orch.config(); + assert_eq!(cfg.projects.len(), 2); + let project_ids: Vec<&str> = cfg.projects.iter().map(|p| p.id.as_str()).collect(); + assert!(project_ids.contains(&"alpha")); + assert!(project_ids.contains(&"beta")); + assert_eq!(cfg.agents.len(), 2); +} + +#[test] +fn accepts_valid_legacy_config_at_startup() { + let orch = AgentOrchestrator::from_config_file(fixture("valid_legacy.toml")) + .expect("valid legacy config should load"); + + let cfg = orch.config(); + assert!( + cfg.projects.is_empty(), + "legacy config should have no projects" + ); + assert_eq!(cfg.agents.len(), 1); + assert!(cfg.agents[0].project.is_none()); +} diff --git a/crates/terraphim_orchestrator/tests/scheduler_tests.rs b/crates/terraphim_orchestrator/tests/scheduler_tests.rs index 098599bb2..ad6fb5611 100644 --- a/crates/terraphim_orchestrator/tests/scheduler_tests.rs +++ b/crates/terraphim_orchestrator/tests/scheduler_tests.rs @@ -23,6 +23,8 @@ fn make_agent(name: &str, layer: AgentLayer, schedule: Option<&str>) -> AgentDef pre_check: None, gitea_issue: None, + + project: None, } } diff --git a/crates/terraphim_service/src/auto_route.rs b/crates/terraphim_service/src/auto_route.rs new file mode 100644 index 000000000..faba9d918 --- /dev/null +++ b/crates/terraphim_service/src/auto_route.rs @@ -0,0 +1,221 @@ +//! Intent-based role auto-routing. +//! +//! Scores every configured role's in-memory `RoleGraph` against the query and +//! returns the role with the highest rank-weighted match. Used by both the CLI +//! (`terraphim-agent search` without `--role`) and the MCP server's `search` +//! tool when the `role` argument is unset. +//! +//! Design: `docs/research/design-intent-based-role-auto-routing.md`. +//! +//! # Locking +//! +//! Acquires every `RoleGraphSync` mutex sequentially. Lock-hold time on these +//! mutexes is now part of routing latency; long-running writers (re-indexing, +//! bulk ingest) will serialise routing behind them. +//! +//! # PA / JMAP downweight +//! +//! When a role has any `ServiceType::Jmap` haystack and `$JMAP_ACCESS_TOKEN` +//! is unset, [`JMAP_MISSING_TOKEN_PENALTY`] is subtracted (saturating at zero) +//! from its raw distinct-concept score. This is a per-haystack-type policy: +//! future additions to `ServiceType` that also need ambient credentials should +//! consider applying the same penalty. + +use ahash::AHashSet; +use terraphim_config::{Config, ConfigState, ServiceType}; +use terraphim_rolegraph::RoleGraph; +use terraphim_types::RoleName; + +/// Penalty subtracted (saturating at zero) from a role's raw distinct-concept +/// score when the role has any `ServiceType::Jmap` haystack and +/// `$JMAP_ACCESS_TOKEN` is not set. +/// +/// Distinct-concept scores typically fall in `0..=10`; multiplicative +/// downweights collapse at those magnitudes (see design section 2.3). +/// Subtraction keeps the policy monotonic without rounding pathology. +pub const JMAP_MISSING_TOKEN_PENALTY: i64 = 1; + +/// Count distinct canonical concept IDs from `rg.thesaurus` that any substring +/// of `query` matches. Uses the rolegraph's pre-built Aho-Corasick automaton +/// (populated from the thesaurus at `RoleGraph::new_sync` time and requires +/// no document indexing). Returns 0 when the thesaurus is empty or no concept +/// matches. +fn score_distinct_concepts(rg: &RoleGraph, query: &str) -> usize { + let ids = rg.find_matching_node_ids(query); + let unique: AHashSet = ids.into_iter().collect(); + unique.len() +} + +/// Inputs the helper needs that are not part of `Config`/`ConfigState`. +#[derive(Debug, Clone)] +pub struct AutoRouteContext { + /// The persisted `selected_role` if it is non-empty AND present in + /// `config.roles`. Callers should normalise via [`AutoRouteContext::from_env`] + /// or by hand before constructing this struct. + pub selected_role: Option, + /// Whether `$JMAP_ACCESS_TOKEN` is set and non-empty. + pub jmap_token_present: bool, +} + +impl AutoRouteContext { + /// Build a context from the process environment. + /// + /// Reads `$JMAP_ACCESS_TOKEN` once. The caller is responsible for + /// normalising `selected_role` against `config.roles` before calling. + pub fn from_env(selected_role: Option) -> Self { + let jmap_token_present = std::env::var("JMAP_ACCESS_TOKEN") + .map(|v| !v.trim().is_empty()) + .unwrap_or(false); + Self { + selected_role, + jmap_token_present, + } + } +} + +/// Why the helper picked the role it did. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AutoRouteReason { + /// One role had the strictly highest score. + ScoredWinner, + /// Multiple roles tied at the top score and `selected_role` was among them. + TieBrokenBySelectedRole, + /// Multiple roles tied at the top score and the alphabetically first was picked. + TieBrokenAlphabetically, + /// All roles scored zero and `selected_role` was set; that role was returned. + ZeroMatchSelectedRole, + /// All roles scored zero and `selected_role` was unset; `Default` (or, if + /// `Default` is absent, the alphabetically first role) was returned. + ZeroMatchDefault, +} + +/// Result of a routing decision. +#[derive(Debug, Clone)] +pub struct AutoRouteResult { + /// The chosen role. + pub role: RoleName, + /// The chosen role's final score (post-penalty). Semantically the count + /// of distinct canonical concept IDs in the role's thesaurus that the + /// query touched, optionally reduced by [`JMAP_MISSING_TOKEN_PENALTY`]. + pub score: i64, + /// All scored candidates including zero-scored, sorted by `(-score, name)`. + pub candidates: Vec<(RoleName, i64)>, + /// Why this role was chosen. + pub reason: AutoRouteReason, +} + +/// Choose a role for `query` by scoring each in-memory rolegraph. +/// +/// Returns a concrete role per the policies in section 3 of the design. +/// The function never errors; in degenerate cases (no roles configured) +/// returns a synthesised result with `RoleName::from("Default")` and reason +/// `ZeroMatchDefault`. +pub async fn auto_select_role( + query: &str, + config: &Config, + state: &ConfigState, + ctx: &AutoRouteContext, +) -> AutoRouteResult { + // Score every role in state.roles. Lock each rolegraph sequentially. + // Scoring is "distinct canonical concept count" against the role's + // thesaurus-driven Aho-Corasick automaton; this works cold (no document + // indexing required), unlike the prior Node.rank sum. + let mut scored: Vec<(RoleName, i64)> = Vec::with_capacity(state.roles.len()); + for (role_name, rg_sync) in state.roles.iter() { + let rg = rg_sync.lock().await; + let raw_score: i64 = score_distinct_concepts(&rg, query) as i64; + + // PA / JMAP penalty: applied to role total, not per-term. + let has_jmap = config + .roles + .get(role_name) + .map(|r| r.haystacks.iter().any(|h| h.service == ServiceType::Jmap)) + .unwrap_or(false); + + let final_score: i64 = if has_jmap && !ctx.jmap_token_present { + raw_score.saturating_sub(JMAP_MISSING_TOKEN_PENALTY) + } else { + raw_score + }; + + scored.push((role_name.clone(), final_score)); + } + + // Sort by (-score, name asc) so candidates[0] is the natural winner and + // ties are broken alphabetically. + scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.original.cmp(&b.0.original))); + + // Degenerate: no roles configured. + if scored.is_empty() { + return AutoRouteResult { + role: RoleName::from("Default"), + score: 0, + candidates: Vec::new(), + reason: AutoRouteReason::ZeroMatchDefault, + }; + } + + let top_score = scored[0].1; + + // Zero-match path. + if top_score == 0 { + if let Some(sel) = ctx.selected_role.as_ref() { + if config.roles.contains_key(sel) { + return AutoRouteResult { + role: sel.clone(), + score: 0, + candidates: scored, + reason: AutoRouteReason::ZeroMatchSelectedRole, + }; + } + } + // Fall back to Default if present, else the alphabetically first role. + let default_role = RoleName::from("Default"); + let chosen = if config.roles.contains_key(&default_role) { + default_role + } else { + scored[0].0.clone() + }; + return AutoRouteResult { + role: chosen, + score: 0, + candidates: scored, + reason: AutoRouteReason::ZeroMatchDefault, + }; + } + + // Collect the tied set (all roles sharing top_score). + let tied: Vec<&RoleName> = scored + .iter() + .filter(|(_, s)| *s == top_score) + .map(|(n, _)| n) + .collect(); + + if tied.len() == 1 { + return AutoRouteResult { + role: scored[0].0.clone(), + score: top_score, + candidates: scored, + reason: AutoRouteReason::ScoredWinner, + }; + } + + // Tie-break: prefer selected_role if it's in the tied set. + if let Some(sel) = ctx.selected_role.as_ref() { + if tied.contains(&sel) { + return AutoRouteResult { + role: sel.clone(), + score: top_score, + candidates: scored, + reason: AutoRouteReason::TieBrokenBySelectedRole, + }; + } + } + + AutoRouteResult { + role: scored[0].0.clone(), + score: top_score, + candidates: scored, + reason: AutoRouteReason::TieBrokenAlphabetically, + } +} diff --git a/crates/terraphim_service/src/lib.rs b/crates/terraphim_service/src/lib.rs index 9ccb0ca43..176d0986e 100644 --- a/crates/terraphim_service/src/lib.rs +++ b/crates/terraphim_service/src/lib.rs @@ -13,6 +13,12 @@ use terraphim_types::{ mod score; use crate::score::Query; +pub mod auto_route; +pub use auto_route::{ + AutoRouteContext, AutoRouteReason, AutoRouteResult, JMAP_MISSING_TOKEN_PENALTY, + auto_select_role, +}; + #[cfg(feature = "openrouter")] pub mod openrouter; diff --git a/crates/terraphim_service/tests/auto_route.rs b/crates/terraphim_service/tests/auto_route.rs new file mode 100644 index 000000000..0b687029e --- /dev/null +++ b/crates/terraphim_service/tests/auto_route.rs @@ -0,0 +1,396 @@ +//! Tests for `terraphim_service::auto_route::auto_select_role`. +//! +//! See `docs/research/design-auto-route-cold-start.md` section 5 for the +//! T1'-T11' cases. These tests construct `Config` + `ConfigState` by hand so +//! they do not depend on `$JMAP_ACCESS_TOKEN` (cargo runs tests in parallel; +//! reading the env var would make outcomes non-deterministic). Use `from_env` +//! only at call sites, never inside unit tests. +//! +//! Scoring is "distinct canonical concept count" against the role's +//! thesaurus-driven Aho-Corasick automaton. No document seeding is required; +//! the prior `insert_document` loop has been dropped to exercise cold-start. + +use ahash::AHashMap; +use std::sync::Arc; +use terraphim_config::{Config, ConfigId, ConfigState, Haystack, Role, ServiceType}; +use terraphim_rolegraph::{RoleGraph, RoleGraphSync}; +use terraphim_service::auto_route::{AutoRouteContext, AutoRouteReason, auto_select_role}; +use terraphim_types::{ + NormalizedTerm, NormalizedTermValue, RelevanceFunction, RoleName, Thesaurus, +}; +use tokio::sync::Mutex; + +/// Build a thesaurus with the given (synonym, id, concept) triples. +fn build_thesaurus(name: &str, terms: &[(&str, u64, &str)]) -> Thesaurus { + let mut t = Thesaurus::new(name.to_string()); + for (synonym, id, concept) in terms { + t.insert( + NormalizedTermValue::from(*synonym), + NormalizedTerm::new(*id, NormalizedTermValue::from(*concept)), + ); + } + t +} + +/// Build a `RoleGraphSync` for `role_name` from a thesaurus alone. +/// No document seeding -- routing now depends only on the prebuilt +/// Aho-Corasick automaton (`RoleGraph::new`), not on indexed documents. +async fn build_rolegraph(role_name: &RoleName, thesaurus: Thesaurus) -> RoleGraphSync { + let rg = RoleGraph::new(role_name.clone(), thesaurus).await.unwrap(); + RoleGraphSync::from(rg) +} + +/// Build a `Role` for the config (TerraphimGraph + optional JMAP haystack). +fn make_role(name: &str, has_jmap: bool) -> Role { + let mut role = Role::new(RoleName::new(name)); + role.relevance_function = RelevanceFunction::TerraphimGraph; + if has_jmap { + role.haystacks.push(Haystack::new( + "jmap://test".to_string(), + ServiceType::Jmap, + true, + )); + } + role +} + +/// Bundle a config + manually-populated ConfigState. Bypasses `ConfigState::new` +/// so tests do not have to register thesauri to disk or fetch over the network. +struct Fixture { + config: Config, + state: ConfigState, +} + +fn assemble(roles: Vec<(Role, RoleGraphSync)>, default: &str, selected: &str) -> Fixture { + let mut role_map = AHashMap::new(); + let mut rg_map = AHashMap::new(); + for (role, rg) in roles { + role_map.insert(role.name.clone(), role.clone()); + rg_map.insert(role.name.clone(), rg); + } + let config = Config { + id: ConfigId::Embedded, + global_shortcut: "Ctrl+X".to_string(), + roles: role_map, + default_role: RoleName::new(default), + selected_role: RoleName::new(selected), + }; + let state = ConfigState { + config: Arc::new(Mutex::new(config.clone())), + roles: rg_map, + }; + Fixture { config, state } +} + +// --------------------------------------------------------------------------- +// T1': Single role wins clearly +// --------------------------------------------------------------------------- +#[tokio::test] +async fn t1_single_role_wins_clearly() { + let sysop_name = RoleName::new("System Operator"); + let default_name = RoleName::new("Default"); + + let sysop_thes = build_thesaurus( + "sysop", + &[ + ("rfp", 1, "acquisition need"), + ("acquisition", 1, "acquisition need"), + ], + ); + let default_thes = build_thesaurus("default", &[("anything", 2, "anything")]); + + let sysop_rg = build_rolegraph(&sysop_name, sysop_thes).await; + let default_rg = build_rolegraph(&default_name, default_thes).await; + + let fixture = assemble( + vec![ + (make_role("System Operator", false), sysop_rg), + (make_role("Default", false), default_rg), + ], + "Default", + "Default", + ); + + let ctx = AutoRouteContext { + selected_role: Some(default_name.clone()), + jmap_token_present: true, + }; + let result = auto_select_role("RFP", &fixture.config, &fixture.state, &ctx).await; + + assert_eq!(result.role.as_str(), "System Operator"); + assert_eq!(result.score, 1); + assert_eq!(result.reason, AutoRouteReason::ScoredWinner); +} + +// --------------------------------------------------------------------------- +// T2': Tie -- selected_role wins +// --------------------------------------------------------------------------- +#[tokio::test] +async fn t2_tie_selected_role_wins() { + let a_name = RoleName::new("Personal Assistant"); + let b_name = RoleName::new("Terraphim Engineer"); + + // Same single matching concept -> identical distinct-concept score (1). + let thes_a = build_thesaurus("a", &[("widget", 10, "widget")]); + let thes_b = build_thesaurus("b", &[("widget", 20, "widget")]); + let rg_a = build_rolegraph(&a_name, thes_a).await; + let rg_b = build_rolegraph(&b_name, thes_b).await; + + let fixture = assemble( + vec![ + (make_role("Personal Assistant", false), rg_a), + (make_role("Terraphim Engineer", false), rg_b), + ], + "Default", + "Terraphim Engineer", + ); + + let ctx = AutoRouteContext { + selected_role: Some(b_name.clone()), + jmap_token_present: true, + }; + let result = auto_select_role("widget", &fixture.config, &fixture.state, &ctx).await; + + assert_eq!(result.role.as_str(), "Terraphim Engineer"); + assert_eq!(result.reason, AutoRouteReason::TieBrokenBySelectedRole); + assert_eq!(result.candidates[0].1, result.candidates[1].1); + assert_eq!(result.score, 1); +} + +// --------------------------------------------------------------------------- +// T3': Tie -- alphabetical +// --------------------------------------------------------------------------- +#[tokio::test] +async fn t3_tie_alphabetical() { + let a_name = RoleName::new("Personal Assistant"); + let b_name = RoleName::new("Terraphim Engineer"); + + let thes_a = build_thesaurus("a", &[("widget", 10, "widget")]); + let thes_b = build_thesaurus("b", &[("widget", 20, "widget")]); + let rg_a = build_rolegraph(&a_name, thes_a).await; + let rg_b = build_rolegraph(&b_name, thes_b).await; + + let fixture = assemble( + vec![ + (make_role("Personal Assistant", false), rg_a), + (make_role("Terraphim Engineer", false), rg_b), + ], + "Default", + "Personal Assistant", + ); + + // Selected role is NOT in the tied set -> alphabetical fallback. + let outsider = RoleName::new("Other Role Not In Config"); + let ctx = AutoRouteContext { + selected_role: Some(outsider), + jmap_token_present: true, + }; + let result = auto_select_role("widget", &fixture.config, &fixture.state, &ctx).await; + + assert_eq!(result.role.as_str(), "Personal Assistant"); + assert_eq!(result.reason, AutoRouteReason::TieBrokenAlphabetically); +} + +// --------------------------------------------------------------------------- +// T4': Zero match, selected_role set +// --------------------------------------------------------------------------- +#[tokio::test] +async fn t4_zero_match_selected_role() { + let rust_name = RoleName::new("Rust Engineer"); + let default_name = RoleName::new("Default"); + + let rust_thes = build_thesaurus("rust", &[("rust", 1, "rust")]); + let default_thes = build_thesaurus("default", &[("anything", 2, "anything")]); + + let rg_rust = build_rolegraph(&rust_name, rust_thes).await; + let rg_default = build_rolegraph(&default_name, default_thes).await; + + let fixture = assemble( + vec![ + (make_role("Rust Engineer", false), rg_rust), + (make_role("Default", false), rg_default), + ], + "Default", + "Rust Engineer", + ); + + let ctx = AutoRouteContext { + selected_role: Some(rust_name.clone()), + jmap_token_present: true, + }; + let result = auto_select_role("xyzzy", &fixture.config, &fixture.state, &ctx).await; + + assert_eq!(result.role.as_str(), "Rust Engineer"); + assert_eq!(result.reason, AutoRouteReason::ZeroMatchSelectedRole); + assert_eq!(result.score, 0); +} + +// --------------------------------------------------------------------------- +// T5': Zero match, selected_role unset +// --------------------------------------------------------------------------- +#[tokio::test] +async fn t5_zero_match_default() { + let default_name = RoleName::new("Default"); + let other_name = RoleName::new("Other"); + + let default_thes = build_thesaurus("default", &[("anything", 1, "anything")]); + let other_thes = build_thesaurus("other", &[("anything_else", 2, "anything_else")]); + + let rg_default = build_rolegraph(&default_name, default_thes).await; + let rg_other = build_rolegraph(&other_name, other_thes).await; + + let fixture = assemble( + vec![ + (make_role("Default", false), rg_default), + (make_role("Other", false), rg_other), + ], + "Default", + "Default", + ); + + let ctx = AutoRouteContext { + selected_role: None, + jmap_token_present: true, + }; + let result = auto_select_role("xyzzy", &fixture.config, &fixture.state, &ctx).await; + + assert_eq!(result.role.as_str(), "Default"); + assert_eq!(result.reason, AutoRouteReason::ZeroMatchDefault); + assert_eq!(result.score, 0); +} + +// --------------------------------------------------------------------------- +// T6': PA loses to a stronger rival under JMAP missing-token penalty +// --------------------------------------------------------------------------- +#[tokio::test] +async fn t6_pa_loses_to_stronger_rival() { + let pa_name = RoleName::new("Personal Assistant"); + let sysop_name = RoleName::new("System Operator"); + + // PA matches one concept; sysop matches two distinct concepts. + let pa_thes = build_thesaurus("pa", &[("invoice", 1, "invoice")]); + let sysop_thes = build_thesaurus( + "sysop", + &[("invoice", 2, "invoice"), ("procurement", 3, "procurement")], + ); + + let rg_pa = build_rolegraph(&pa_name, pa_thes).await; + let rg_sysop = build_rolegraph(&sysop_name, sysop_thes).await; + + let fixture = assemble( + vec![ + (make_role("Personal Assistant", true), rg_pa), + (make_role("System Operator", false), rg_sysop), + ], + "Default", + "Personal Assistant", + ); + + let ctx = AutoRouteContext { + selected_role: Some(pa_name.clone()), + jmap_token_present: false, + }; + let result = + auto_select_role("invoice procurement", &fixture.config, &fixture.state, &ctx).await; + + // Raw: PA=1, sysop=2. After penalty: PA=0, sysop=2. Sysop wins outright. + assert_eq!(result.role.as_str(), "System Operator"); + assert_eq!(result.score, 2); + assert_eq!(result.reason, AutoRouteReason::ScoredWinner); +} + +// --------------------------------------------------------------------------- +// T7': PA wins with sufficient evidence (penalty does not silence it) +// --------------------------------------------------------------------------- +#[tokio::test] +async fn t7_pa_wins_when_only_pa_matches() { + let pa_name = RoleName::new("Personal Assistant"); + let other_name = RoleName::new("Default"); + + // PA matches two distinct concepts; rival matches none. + let pa_thes = build_thesaurus( + "pa", + &[("invoice", 1, "invoice"), ("receipt", 2, "receipt")], + ); + let other_thes = build_thesaurus("default", &[("rust", 3, "rust")]); + + let rg_pa = build_rolegraph(&pa_name, pa_thes).await; + let rg_other = build_rolegraph(&other_name, other_thes).await; + + let fixture = assemble( + vec![ + (make_role("Personal Assistant", true), rg_pa), + (make_role("Default", false), rg_other), + ], + "Default", + "Personal Assistant", + ); + + let ctx = AutoRouteContext { + selected_role: Some(pa_name.clone()), + jmap_token_present: false, + }; + let result = + auto_select_role("invoice and receipt", &fixture.config, &fixture.state, &ctx).await; + + // Raw PA=2, after penalty=1. Rival raw=0. PA still wins. + assert_eq!(result.role.as_str(), "Personal Assistant"); + assert_eq!(result.score, 1); + assert_eq!(result.reason, AutoRouteReason::ScoredWinner); +} + +// --------------------------------------------------------------------------- +// T11': Cold-start regression -- the headline test for #617. +// +// Reproduces the production cold-start scenario: a `Config` shaped like +// `~/.config/terraphim/embedded_config.json` with a "System Operator" role +// whose thesaurus maps `rfp -> rfp`, but with `RoleGraph::new` called on +// thesaurus only -- `insert_document` is NEVER called. Against the prior +// rank-sum scorer this returned score=0 across all roles and Default won by +// fallback; against the new distinct-concept scorer it must return +// "System Operator" with score >= 1. +// --------------------------------------------------------------------------- +#[tokio::test] +async fn t11_cold_start_no_documents_indexed() { + let sysop_name = RoleName::new("System Operator"); + let default_name = RoleName::new("Default"); + let engineer_name = RoleName::new("Terraphim Engineer"); + + let sysop_thes = build_thesaurus("sysop", &[("rfp", 1, "rfp")]); + let default_thes = build_thesaurus("default", &[("readme", 2, "readme")]); + let engineer_thes = build_thesaurus("engineer", &[("crate", 3, "crate")]); + + // Cold start: build rolegraphs from thesaurus only -- no insert_document. + let sysop_rg = build_rolegraph(&sysop_name, sysop_thes).await; + let default_rg = build_rolegraph(&default_name, default_thes).await; + let engineer_rg = build_rolegraph(&engineer_name, engineer_thes).await; + + let fixture = assemble( + vec![ + (make_role("System Operator", false), sysop_rg), + (make_role("Default", false), default_rg), + (make_role("Terraphim Engineer", false), engineer_rg), + ], + "Default", + "Default", + ); + + // No --role override; selected_role is "Default" (matches embedded_config). + let ctx = AutoRouteContext { + selected_role: Some(default_name.clone()), + jmap_token_present: true, + }; + let result = auto_select_role("RFP", &fixture.config, &fixture.state, &ctx).await; + + assert_eq!( + result.role.as_str(), + "System Operator", + "cold-start: System Operator must win on 'RFP' without document indexing" + ); + assert!( + result.score >= 1, + "cold-start: score must be >= 1 (was {}). Prior rank-sum scorer would have returned 0.", + result.score + ); + assert_eq!(result.reason, AutoRouteReason::ScoredWinner); +} diff --git a/crates/terraphim_sessions/src/connector/aider.rs b/crates/terraphim_sessions/src/connector/aider.rs index 5abc8bab0..b26c05e5d 100644 --- a/crates/terraphim_sessions/src/connector/aider.rs +++ b/crates/terraphim_sessions/src/connector/aider.rs @@ -133,8 +133,7 @@ impl AiderConnector { .and_then(|m| m.modified()) .ok() .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|d| DateTime::from_timestamp(d.as_secs() as i64, 0)) - .flatten() + .and_then(|d| DateTime::from_timestamp(d.as_secs() as i64, 0)) .unwrap_or_else(Utc::now) }); diff --git a/crates/terraphim_spawner/src/config.rs b/crates/terraphim_spawner/src/config.rs index 0b3213cf3..c671576f8 100644 --- a/crates/terraphim_spawner/src/config.rs +++ b/crates/terraphim_spawner/src/config.rs @@ -109,6 +109,10 @@ impl AgentConfig { "--format".to_string(), "json".to_string(), ], + // Shell interpreters: pass the task as an inline script. Enables + // shell-script agents like fleet-meta to run `cli_tool = "/bin/bash"` + // with the task body as the script source. + "bash" | "sh" => vec!["-c".to_string()], _ => Vec::new(), } } @@ -381,4 +385,11 @@ mod tests { let args = AgentConfig::model_args("/home/alex/.bun/bin/opencode", "opencode-go/kimi-k2.5"); assert_eq!(args, vec!["-m", "opencode-go/kimi-k2.5"]); } + + #[test] + fn test_infer_args_bash_uses_dash_c() { + assert_eq!(AgentConfig::infer_args("/bin/bash"), vec!["-c"]); + assert_eq!(AgentConfig::infer_args("bash"), vec!["-c"]); + assert_eq!(AgentConfig::infer_args("/usr/bin/sh"), vec!["-c"]); + } } diff --git a/crates/terraphim_spawner/src/lib.rs b/crates/terraphim_spawner/src/lib.rs index 4cd4088ae..acd1b9e67 100644 --- a/crates/terraphim_spawner/src/lib.rs +++ b/crates/terraphim_spawner/src/lib.rs @@ -17,6 +17,39 @@ use tokio::time::timeout; use terraphim_types::capability::{ProcessId, Provider}; +/// Per-spawn overrides that a caller can pass to AgentSpawner::spawn(). +/// +/// Enables multi-project use: one orchestrator serving many projects can +/// pass per-project working_dir and env without constructing N spawners. +#[derive(Debug, Clone, Default)] +pub struct SpawnContext { + /// Working directory for the child process. None -> use spawner default. + pub working_dir: Option, + /// Env vars to set on the child process (added to inherited env). + pub env_overrides: HashMap, +} + +impl SpawnContext { + /// Use the spawner's default working_dir and no env overrides. + pub fn global() -> Self { + Self::default() + } + + /// Override working_dir; keep env untouched. + pub fn with_working_dir(path: impl Into) -> Self { + Self { + working_dir: Some(path.into()), + env_overrides: HashMap::new(), + } + } + + /// Builder-style env addition. + pub fn with_env(mut self, key: impl Into, value: impl Into) -> Self { + self.env_overrides.insert(key.into(), value.into()); + self + } +} + pub mod audit; pub mod config; pub mod health; @@ -421,13 +454,15 @@ impl AgentSpawner { provider: &Provider, task: &str, model: Option<&str>, + ctx: SpawnContext, ) -> Result { let config = AgentConfig::from_provider(provider)?; let config = match model { Some(m) => config.with_model(m), None => config, }; - self.spawn_config(provider, &config, task, false).await + self.spawn_config(provider, &config, task, false, &ctx) + .await } /// Spawn an agent from a provider configuration with an optional model, @@ -437,13 +472,14 @@ impl AgentSpawner { provider: &Provider, task: &str, model: Option<&str>, + ctx: SpawnContext, ) -> Result { let config = AgentConfig::from_provider(provider)?; let config = match model { Some(m) => config.with_model(m), None => config, }; - self.spawn_config(provider, &config, task, true).await + self.spawn_config(provider, &config, task, true, &ctx).await } /// Internal: spawn with model, stdin option, and resource limits. @@ -454,6 +490,7 @@ impl AgentSpawner { model: Option<&str>, use_stdin: bool, resource_limits: ResourceLimits, + ctx: &SpawnContext, ) -> Result { let config = AgentConfig::from_provider(provider)?; let config = config.with_resource_limits(resource_limits); @@ -461,17 +498,20 @@ impl AgentSpawner { Some(m) => config.with_model(m), None => config, }; - self.spawn_config(provider, &config, task, use_stdin).await + self.spawn_config(provider, &config, task, use_stdin, ctx) + .await } - /// Spawn an agent from a provider configuration + /// Spawn an agent from a provider configuration. pub async fn spawn( &self, provider: &Provider, task: &str, + ctx: SpawnContext, ) -> Result { let config = AgentConfig::from_provider(provider)?; - self.spawn_config(provider, &config, task, false).await + self.spawn_config(provider, &config, task, false, &ctx) + .await } /// Spawn an agent with primary and fallback configuration. @@ -481,6 +521,7 @@ impl AgentSpawner { pub async fn spawn_with_fallback( &self, request: &SpawnRequest, + ctx: SpawnContext, ) -> Result { // Try primary first with resource limits let primary_result = self @@ -490,6 +531,7 @@ impl AgentSpawner { request.primary_model.as_deref(), request.use_stdin, request.resource_limits.clone(), + &ctx, ) .await; @@ -517,6 +559,7 @@ impl AgentSpawner { request.fallback_model.as_deref(), request.use_stdin, request.resource_limits.clone(), + &ctx, ) .await; @@ -553,6 +596,7 @@ impl AgentSpawner { config: &AgentConfig, task: &str, use_stdin: bool, + ctx: &SpawnContext, ) -> Result { let _span = tracing::info_span!( "spawner.spawn", @@ -566,7 +610,7 @@ impl AgentSpawner { // Spawn the agent process let process_id = ProcessId::new(); - let mut child = self.spawn_process(config, task, use_stdin).await?; + let mut child = self.spawn_process(config, task, use_stdin, ctx).await?; // Set up health checking let health_checker = HealthChecker::new(process_id, Duration::from_secs(30)); @@ -608,10 +652,13 @@ impl AgentSpawner { config: &AgentConfig, task: &str, use_stdin: bool, + ctx: &SpawnContext, ) -> Result { - let working_dir = config + // Priority: ctx override > config working_dir > spawner default + let working_dir = ctx .working_dir .as_ref() + .or(config.working_dir.as_ref()) .unwrap_or(&self.default_working_dir); let mut cmd = Command::new(&config.cli_command); @@ -626,7 +673,7 @@ impl AgentSpawner { cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); - // Add environment variables + // Add environment variables (spawner defaults, lowest priority) for (key, value) in &self.env_vars { cmd.env(key, value); } @@ -636,6 +683,11 @@ impl AgentSpawner { cmd.env(key, value); } + // Apply per-call overrides last (highest priority) + for (key, value) in &ctx.env_overrides { + cmd.env(key, value); + } + // Strip ANTHROPIC_API_KEY for Claude CLI agents. // Claude CLI uses OAuth (browser flow) for authentication. // If ANTHROPIC_API_KEY is set in the environment (even inherited), @@ -763,7 +815,9 @@ mod tests { let spawner = AgentSpawner::new(); let provider = create_test_agent_provider(); - let handle = spawner.spawn(&provider, "Hello World").await; + let handle = spawner + .spawn(&provider, "Hello World", SpawnContext::global()) + .await; // Echo command should succeed assert!(handle.is_ok()); @@ -777,7 +831,10 @@ mod tests { let spawner = AgentSpawner::new(); let provider = create_test_agent_provider(); - let mut handle = spawner.spawn(&provider, "done").await.unwrap(); + let mut handle = spawner + .spawn(&provider, "done", SpawnContext::global()) + .await + .unwrap(); // Echo exits immediately; give it a moment tokio::time::sleep(Duration::from_millis(100)).await; @@ -793,7 +850,10 @@ mod tests { let provider = create_sleep_agent_provider(); // Spawn a sleep 60 agent - let mut handle = spawner.spawn(&provider, "60").await.unwrap(); + let mut handle = spawner + .spawn(&provider, "60", SpawnContext::global()) + .await + .unwrap(); // Graceful shutdown with 2s grace period let result = handle.shutdown(Duration::from_secs(2)).await; @@ -817,7 +877,10 @@ mod tests { let spawner = AgentSpawner::new(); let provider = create_test_agent_provider(); - let handle = spawner.spawn(&provider, "hello").await.unwrap(); + let handle = spawner + .spawn(&provider, "hello", SpawnContext::global()) + .await + .unwrap(); let mut pool = AgentPool::new(5); pool.release(handle); @@ -834,7 +897,10 @@ mod tests { let spawner = AgentSpawner::new(); let provider = create_test_agent_provider(); - let handle = spawner.spawn(&provider, "broadcast test").await.unwrap(); + let handle = spawner + .spawn(&provider, "broadcast test", SpawnContext::global()) + .await + .unwrap(); let mut receiver = handle.subscribe_output(); // Give the echo process time to produce output and the capture task to process it @@ -864,7 +930,9 @@ mod tests { // Spawn with resource limits -- echo exits fast so this validates // that the pre_exec hook with setrlimit doesn't break spawning. - let handle = spawner.spawn(&provider, "resource-limited").await; + let handle = spawner + .spawn(&provider, "resource-limited", SpawnContext::global()) + .await; assert!(handle.is_ok()); } @@ -873,7 +941,10 @@ mod tests { let spawner = AgentSpawner::new(); let provider = create_test_agent_provider(); - let handle = spawner.spawn(&provider, "hello").await.unwrap(); + let handle = spawner + .spawn(&provider, "hello", SpawnContext::global()) + .await + .unwrap(); let mut pool = AgentPool::new(5); pool.release(handle); @@ -908,7 +979,7 @@ mod tests { // Spawn with stdin delivery - cat will echo the prompt back let handle = spawner - .spawn_with_model_stdin(&provider, "hello from stdin", None) + .spawn_with_model_stdin(&provider, "hello from stdin", None, SpawnContext::global()) .await; assert!(handle.is_ok()); @@ -943,7 +1014,9 @@ mod tests { let provider = create_test_agent_provider(); // Spawn without stdin - prompt should be CLI arg - let handle = spawner.spawn(&provider, "arg test").await; + let handle = spawner + .spawn(&provider, "arg test", SpawnContext::global()) + .await; assert!(handle.is_ok()); @@ -978,7 +1051,7 @@ mod tests { // Spawn with stdin - should complete without error let handle = spawner - .spawn_with_model_stdin(&provider, &large_prompt, None) + .spawn_with_model_stdin(&provider, &large_prompt, None, SpawnContext::global()) .await; assert!( @@ -1009,7 +1082,12 @@ mod tests { // Spawn with both model and stdin let handle = spawner - .spawn_with_model_stdin(&provider, "model test via stdin", Some("test-model")) + .spawn_with_model_stdin( + &provider, + "model test via stdin", + Some("test-model"), + SpawnContext::global(), + ) .await; assert!(handle.is_ok()); @@ -1037,4 +1115,208 @@ mod tests { Some(2_147_483_648) ); } + + // ========================================================================= + // SpawnContext Tests (Gitea adf-fleet#3) + // ========================================================================= + + #[test] + fn test_spawn_context_global_is_default() { + let ctx = SpawnContext::global(); + assert!(ctx.working_dir.is_none()); + assert!(ctx.env_overrides.is_empty()); + } + + #[test] + fn test_spawn_context_with_working_dir() { + let ctx = SpawnContext::with_working_dir("/some/project"); + assert_eq!(ctx.working_dir, Some(PathBuf::from("/some/project"))); + assert!(ctx.env_overrides.is_empty()); + } + + #[test] + fn test_spawn_context_with_env() { + let ctx = SpawnContext::global() + .with_env("FOO", "bar") + .with_env("BAZ", "qux"); + assert!(ctx.working_dir.is_none()); + assert_eq!(ctx.env_overrides.get("FOO"), Some(&"bar".to_string())); + assert_eq!(ctx.env_overrides.get("BAZ"), Some(&"qux".to_string())); + } + + #[tokio::test] + async fn test_spawn_global_uses_spawner_default_working_dir() { + let spawner = AgentSpawner::new().with_working_dir("/tmp"); + let provider = create_test_agent_provider(); + + // SpawnContext::global() should preserve spawner's default behaviour. + // We spawn /bin/echo (via echo provider) and check it succeeds. + let handle = spawner + .spawn(&provider, "hello", SpawnContext::global()) + .await; + assert!(handle.is_ok(), "spawn with global context should succeed"); + } + + #[tokio::test] + async fn test_spawn_with_working_dir_override() { + use std::os::unix::fs::PermissionsExt; + use tempfile::TempDir; + + let tmpdir = TempDir::new().expect("create tempdir"); + let tmppath = tmpdir.path().to_path_buf(); + + // Write a tiny shell script that sleeps briefly before printing pwd. + // The sleep is load-bearing: it gives the test time to call + // `subscribe_output` after `spawn().await` returns. `OutputCapture` + // uses a tokio broadcast channel which drops messages emitted before + // any subscriber exists, so a fast `/bin/pwd` could complete and + // drop its line on the floor between spawn and subscribe. The + // validator only accepts a single executable path, so we cannot + // pass `sh -c '...'` as cli_command. + let script_path = tmpdir.path().join("pwd-with-delay.sh"); + std::fs::write(&script_path, "#!/bin/sh\nsleep 0.2\npwd\n").expect("write pwd script"); + let mut perms = std::fs::metadata(&script_path).unwrap().permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&script_path, perms).expect("chmod script"); + + let provider = Provider::new( + "@pwd-agent", + "Pwd Agent", + terraphim_types::capability::ProviderType::Agent { + agent_id: "@pwd".to_string(), + cli_command: script_path.to_string_lossy().to_string(), + working_dir: PathBuf::from("/tmp"), + }, + vec![terraphim_types::capability::Capability::CodeGeneration], + ); + + let spawner = AgentSpawner::new().with_working_dir("/tmp"); + let ctx = SpawnContext::with_working_dir(tmppath.clone()); + + let handle = spawner + .spawn(&provider, ".", ctx) + .await + .expect("spawn with working_dir override should succeed"); + + let mut rx = handle.subscribe_output(); + let resolved = std::fs::canonicalize(&tmppath).unwrap_or(tmppath.clone()); + + // Drain output events until either the matching cwd line arrives, + // the broadcast closes (sender dropped after capture finished), or + // a generous overall timeout fires. recv() rather than try_recv() + // blocks for actual output instead of polling-with-sleep. + let deadline = tokio::time::Instant::now() + Duration::from_secs(3); + let mut found = false; + loop { + let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); + if remaining.is_zero() { + break; + } + match tokio::time::timeout(remaining, rx.recv()).await { + Ok(Ok(OutputEvent::Stdout { line, .. })) => { + let trimmed = line.trim(); + if trimmed == resolved.to_string_lossy().as_ref() + || trimmed == tmppath.to_string_lossy().as_ref() + { + found = true; + break; + } + } + Ok(Ok(_)) => {} // non-stdout event; keep draining + Ok(Err(_)) => break, // broadcast closed + Err(_) => break, // timeout + } + } + + assert!(found, "child cwd should be the overridden tmpdir"); + } + + #[tokio::test] + async fn test_spawn_env_override_propagates() { + // Use /usr/bin/printenv VAR_NAME to verify env override. + // printenv takes the variable name as its argument and prints the value. + let provider = Provider::new( + "@printenv-env-agent", + "Printenv Env Agent", + terraphim_types::capability::ProviderType::Agent { + agent_id: "@printenv-env".to_string(), + cli_command: "/usr/bin/printenv".to_string(), + working_dir: PathBuf::from("/tmp"), + }, + vec![terraphim_types::capability::Capability::CodeGeneration], + ); + + let spawner = AgentSpawner::new(); + let ctx = SpawnContext::global().with_env("ADF_SPAWN_CTX_TEST", "hello-from-ctx"); + + // Task "ADF_SPAWN_CTX_TEST" becomes the arg to printenv, printing its value. + let handle = spawner + .spawn(&provider, "ADF_SPAWN_CTX_TEST", ctx) + .await + .expect("spawn with env override should succeed"); + + let mut rx = handle.subscribe_output(); + tokio::time::sleep(Duration::from_millis(300)).await; + + let mut output = String::new(); + loop { + match rx.try_recv() { + Ok(OutputEvent::Stdout { line, .. }) => output.push_str(line.trim()), + Ok(_) => {} + Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break, + Err(_) => break, + } + } + + assert!( + output.contains("hello-from-ctx"), + "env override should be visible in child process, got: {:?}", + output + ); + } + + #[tokio::test] + async fn test_inherited_env_flows_through_without_override() { + // Set an env var in the test process and verify a child sees it. + // Using /usr/bin/printenv VAR_NAME avoids shell argument-parsing issues. + unsafe { + std::env::set_var("ADF_INHERITED_SPAWN_CTX", "inherited-value"); + } + + let provider = Provider::new( + "@printenv-inherit-agent", + "Printenv Inherit Agent", + terraphim_types::capability::ProviderType::Agent { + agent_id: "@printenv-inherit".to_string(), + cli_command: "/usr/bin/printenv".to_string(), + working_dir: PathBuf::from("/tmp"), + }, + vec![terraphim_types::capability::Capability::CodeGeneration], + ); + + let spawner = AgentSpawner::new(); + let handle = spawner + .spawn(&provider, "ADF_INHERITED_SPAWN_CTX", SpawnContext::global()) + .await + .expect("spawn should succeed"); + + let mut rx = handle.subscribe_output(); + tokio::time::sleep(Duration::from_millis(300)).await; + + let mut output = String::new(); + loop { + match rx.try_recv() { + Ok(OutputEvent::Stdout { line, .. }) => output.push_str(line.trim()), + Ok(_) => {} + Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break, + Err(_) => break, + } + } + + assert!( + output.contains("inherited-value"), + "inherited env should be visible in child without override, got: {:?}", + output + ); + } } diff --git a/crates/terraphim_tracker/src/gitea.rs b/crates/terraphim_tracker/src/gitea.rs index 6bb3b6c63..dbd71c7b5 100644 --- a/crates/terraphim_tracker/src/gitea.rs +++ b/crates/terraphim_tracker/src/gitea.rs @@ -131,6 +131,16 @@ impl GiteaTracker { Ok(Self { client, config }) } + /// Gitea owner (org or user) this tracker is scoped to. + pub fn owner(&self) -> &str { + &self.config.owner + } + + /// Gitea repository this tracker is scoped to. + pub fn repo(&self) -> &str { + &self.config.repo + } + /// Build request with authentication. pub(crate) fn build_request( &self, diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 017f53032..9518b0ef5 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -38,6 +38,8 @@ - [Learning Capture for Claude Code](./howto/learning-capture-claude-code.md) - [Learning Capture for opencode](./howto/learning-capture-opencode.md) +- [Personal Assistant Role (JMAP + Obsidian)](./howto/personal-assistant-role.md) +- [Plug Terraphim Search into Claude Code and opencode](./howto/mcp-integration-claude-opencode.md) ## Symphony diff --git a/docs/src/howto/mcp-integration-claude-opencode.md b/docs/src/howto/mcp-integration-claude-opencode.md new file mode 100644 index 000000000..eb7b58f8e --- /dev/null +++ b/docs/src/howto/mcp-integration-claude-opencode.md @@ -0,0 +1,153 @@ +# Plug Terraphim Search into Claude Code and opencode + +Two integration paths. CLI is the recommended starting point: zero new binaries, faster cold start, works in every host that can shell out. MCP earns its keep when you also want autocomplete-as-you-type or `update_config_tool` exposed to the model. Both paths use the **same** `~/.config/terraphim/embedded_config.json`, so you get all six roles (Terraphim Engineer, Personal Assistant, System Operator, Context Engineering Author, Rust Engineer, Default) in either case. + +## Path A -- CLI via slash command (recommended) + +`terraphim-agent search` already exists, takes `--role` and `--limit`, and returns ranked results to stdout. A two-line slash command in either host wraps it. + +### Claude Code + +Create `~/.claude/commands/tsearch.md`: + +```markdown +--- +description: Terraphim search across configured roles. Usage: /tsearch [role] +allowed-tools: Bash(terraphim-agent search:*), Bash(terraphim-agent-pa search:*) +--- +Run `terraphim-agent search --role "" --limit 5 ""` (or +`terraphim-agent-pa search ...` if the role is "Personal Assistant" and +the query needs the JMAP haystack). Return the top results as a numbered +list with title, source path/URL, and a 120-char snippet. +``` + +That is the entire integration. The `allowed-tools` line auto-approves the two CLI invocations so the model does not have to ask permission for each call. + +### opencode + +Same file, drop it at `~/.config/opencode/command/tsearch.md`. opencode reads the same frontmatter shape; no extra setup. + +### Why this is fast enough + +`terraphim-agent` reads the persisted role state at start (low ms), runs the query against the role's haystacks (Aho-Corasick on local KG plus ripgrep over the haystack folders), and returns. For a typical knowledge-graph query against the Terraphim Engineer role on a laptop, the round trip is well under a second from slash command to formatted output. The agent already has the typed CLI (`--role`, `--limit`, `--format json`) -- no need to layer MCP on top for the common case. + +## Path B -- MCP server (when you want typed tools) + +If you want the model to call `search` as a first-class tool with structured JSON output -- alongside `autocomplete_terms`, `autocomplete_with_snippets`, `fuzzy_autocomplete_search`, `build_autocomplete_index`, and `update_config_tool` -- register `terraphim_mcp_server` instead. It reads the same config so the role list is identical. + +### Build the binary + +```bash +cd ~/projects/terraphim/terraphim-ai +cargo build --release -p terraphim_mcp_server --features jmap +cp target/release/terraphim_mcp_server ~/.cargo/bin/terraphim_mcp_server +``` + +For the Personal Assistant role (JMAP needs `JMAP_ACCESS_TOKEN` from 1Password), wrap the binary so the secret never lands on disk. Mirror the existing `terraphim-agent-pa` pattern at `~/bin/terraphim_mcp_server-pa`: + +```bash +#!/usr/bin/env bash +exec op run --account my.1password.com \ + --env-file=<(echo 'JMAP_ACCESS_TOKEN=op://VAULT/ITEM/credential') \ + -- /Users/alex/.cargo/bin/terraphim_mcp_server "$@" +``` + +### Register in opencode + +Add two entries under `mcp` in `~/.config/opencode/opencode.json`: + +```jsonc +"terraphim": { "type": "local", "command": ["/Users/alex/.cargo/bin/terraphim_mcp_server"] }, +"terraphim-pa": { "type": "local", "command": ["/Users/alex/bin/terraphim_mcp_server-pa"] } +``` + +Restart opencode. Tools appear as `mcp__terraphim__search`, `mcp__terraphim_pa__search`, etc. + +### Register in Claude Code + +```bash +claude mcp add terraphim /Users/alex/.cargo/bin/terraphim_mcp_server +claude mcp add terraphim-pa /Users/alex/bin/terraphim_mcp_server-pa +claude mcp list +``` + +The list output should show both servers as Connected. + +## SessionStart primer (both paths) + +Slash commands are useless if the model does not know the roles exist. Extend the SessionStart hook in `~/.claude/settings.json` (and the equivalent in opencode) to print a one-screen role index: + +```bash +printf '\n--- Terraphim search via /tsearch [role] ---\n' +printf ' Terraphim Engineer (Rust/agent KG)\n' +printf ' Personal Assistant (Obsidian + Fastmail JMAP, use terraphim-agent-pa for email)\n' +printf ' System Operator (INCOSE/MBSE Logseq KG)\n' +printf ' Context Engineering Author, Rust Engineer, Default\n' +``` + +## Verify + +CLI path: + +```bash +terraphim-agent search --role "Terraphim Engineer" --limit 3 "rolegraph" +terraphim-agent search --role "System Operator" --limit 3 "RFP" +terraphim-agent-pa search --role "Personal Assistant" --limit 3 "invoice" +``` + +MCP path: + +```bash +claude mcp list | grep terraphim +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \ + | ~/.cargo/bin/terraphim_mcp_server +``` + +Inside a fresh Claude Code or opencode session, ask the model to list available tools; both `mcp__terraphim__search` and `mcp__terraphim_pa__search` should appear if the MCP path is configured. + +## Auto-routing (CLI and MCP) + +Both paths now auto-route the search when no role is specified. The agent scores every configured role's knowledge graph against the query and picks the highest-rank match. + +CLI: skip `--role` and the picked role is printed once on stderr: + +``` +[auto-route] picked role "System Operator" (score=3, candidates=4); to override, pass --role +``` + +stdout (including `--robot` and `--format json` payloads) is unchanged. + +MCP: omit the `role` parameter on the `terraphim_search` tool. The CallToolResult prepends one text content of the form `[auto-route] picked role "" (score=, candidates=); pass role parameter to override` so MCP clients can surface the routing decision; the resource contents that follow are unchanged in count and order. + +Pass an explicit role to short-circuit auto-routing -- the routing line is suppressed entirely. + +## Three example queries (one per role) + +- **Terraphim Engineer**: `/tsearch "Terraphim Engineer" rolegraph` -- returns hits from `~/.config/terraphim/docs/src/`. +- **System Operator**: `/tsearch "System Operator" RFP` -- KG normalises `RFP` to `acquisition need` (INCOSE canonical term) and returns `Acquisition need.md` near the top with a high rank score. +- **Personal Assistant**: `/tsearch "Personal Assistant" invoice` -- mixes Obsidian notes with `jmap:///email/` hits from your Fastmail mailbox. The wrapper script injects `JMAP_ACCESS_TOKEN` once per call. + +## When to pick which path + +| | CLI (Path A) | MCP (Path B) | +| --- | --- | --- | +| New binaries needed | None | `terraphim_mcp_server` + wrapper | +| Cold start | ~50-200 ms per call | ~10-50 ms per call (long-lived process) | +| Tools exposed | `search` only | `search` + 4 autocomplete + `build_autocomplete_index` + `update_config_tool` | +| Works in any host | Yes -- anything that can run a slash command | Only hosts that speak MCP | +| Token handling | Wrapper script (`terraphim-agent-pa`) | Wrapper script (`terraphim_mcp_server-pa`) | + +For the search-across-roles flow, CLI is enough. Add MCP when the model needs autocomplete-as-you-type or you want it to manage role configuration without leaving the conversation. + +## Troubleshooting + +- **`terraphim-agent: command not found`** in slash command output -- the host is shelling out without your shell environment. Either install via `cargo install terraphim_agent` to a globally-visible path, or absolute-path the binary in the slash command (`/Users/alex/.cargo/bin/terraphim-agent ...`). +- **`mcp__terraphim_pa__search` returns zero email hits** -- the `op` CLI needs an active session (`op signin`); biometric prompts may not surface from non-interactive MCP health checks. Run `op signin` once per terminal session before launching the host. +- **No matches from any role** -- run `terraphim-agent config reload` to rebuild the persisted role index from `embedded_config.json`. Most empty-result confusion is a stale persisted snapshot. +- **Personal Assistant only shows notes, never email** -- you are calling `terraphim-agent` instead of `terraphim-agent-pa`, so `JMAP_ACCESS_TOKEN` is unset. The bare CLI is fine for the other roles. + +## Related + +- [Personal Assistant role](./personal-assistant-role.md) -- the JMAP + Obsidian role this integration exposes +- [System Operator README](../../terraphim_server/README_SYSTEM_OPERATOR.md) -- the Logseq MBSE KG +- [Command Rewriting How-To](../command-rewriting-howto.md) -- the hooks-based knowledge-graph integration that runs alongside this search integration diff --git a/docs/src/howto/personal-assistant-role.md b/docs/src/howto/personal-assistant-role.md new file mode 100644 index 000000000..8753febcf --- /dev/null +++ b/docs/src/howto/personal-assistant-role.md @@ -0,0 +1,177 @@ +# Personal Assistant Role: Search Email and Notes Together + +The Personal Assistant role indexes a Fastmail JMAP mailbox and an Obsidian vault under a single Terraphim role, so one query returns both notes and email ordered by knowledge-graph relevance. This how-to shows the end-to-end setup. + +## Why this exists + +Most "personal AI" tools split your context across silos: one search box for email, another for notes, a third for chat history. Terraphim treats every source as a haystack on the same role, so a single query crosses them. The Personal Assistant role wires up the two most common personal sources -- email (JMAP) and notes (Obsidian) -- with deterministic, sub-millisecond ranking and no cloud round-trip. + +## Prerequisites + +1. **A locally built `terraphim-agent` with the `jmap` feature.** The crates.io binary does not include `haystack_jmap` (the dependency is not yet published), so `cargo install terraphim_agent` will index the Obsidian vault but silently skip JMAP. See [Crates Overview](../crates-overview.md) for the workspace layout. + + ```bash + cd ~/projects/terraphim/terraphim-ai + cargo build --release -p terraphim_agent --features jmap + cp target/release/terraphim-agent ~/.cargo/bin/terraphim-agent + ``` + + To enable the feature in your local checkout (the published crates have it commented out): + + ```toml + # crates/terraphim_middleware/Cargo.toml + haystack_jmap = { path = "../haystack_jmap", version = "1.0.0", optional = true } + # in [features] + jmap = ["dep:haystack_jmap"] + + # crates/terraphim_agent/Cargo.toml -- in [features] + jmap = ["terraphim_middleware/jmap"] + ``` + +2. **An Obsidian vault on the local filesystem** -- any path containing markdown files works. This guide assumes `~/synced/ObsidianVault`. + +3. **A Fastmail JMAP access token.** Generate one at with the "Mail" scope. Store it in 1Password (or any secret manager that exposes it via env at runtime) -- never paste it into the role config on disk. + +## Step 1 -- Add the role to `embedded_config.json` + +Back up first: + +```bash +cp ~/.config/terraphim/embedded_config.json{,.bak-$(date +%Y-%m-%d)} +``` + +Add the role under `roles` in `~/.config/terraphim/embedded_config.json`: + +```json +"Personal Assistant": { + "shortname": "PA", + "name": "Personal Assistant", + "relevance_function": "terraphim-graph", + "terraphim_it": false, + "theme": "lumen", + "kg": { + "automata_path": null, + "knowledge_graph_local": { + "input_type": "markdown", + "path": "/Users/alex/synced/ObsidianVault" + }, + "public": false, + "publish": false + }, + "haystacks": [ + { + "location": "/Users/alex/synced/ObsidianVault", + "service": "Ripgrep", + "read_only": true + }, + { + "location": "https://api.fastmail.com/jmap/session", + "service": "Jmap", + "read_only": true, + "extra_parameters": { + "limit": "50" + } + } + ], + "llm_enabled": false +} +``` + +Notes on the choices: + +- The Obsidian haystack uses `Ripgrep` because the vault is just markdown -- no Obsidian-specific service needed. `read_only: true` ensures the agent never edits notes. +- The JMAP haystack `location` defaults to Fastmail's session URL; override it for other JMAP providers. +- `kg.knowledge_graph_local.path` points at the same vault, so the role's knowledge graph is built from your own notes -- this gives the Aho-Corasick matcher synonyms specific to your project vocabulary, which boosts ranking on both notes and email. +- `extra_parameters.limit` caps the JMAP search to 50 hits per query; tune as needed. +- The token is **not** in the JSON. JMAP haystack reads `JMAP_ACCESS_TOKEN` from environment first, then falls back to `extra_parameters.access_token`. We use the env path so the secret never lands on disk. + +Reload the agent's persisted config from the JSON file: + +```bash +terraphim-agent config reload +``` + +Verify the role appears: + +```bash +terraphim-agent roles list +``` + +The output should include `Personal Assistant (PA)` alongside the existing roles. + +## Step 2 -- Wrapper script for token injection + +Because `JMAP_ACCESS_TOKEN` must be set in the agent's environment for every Personal Assistant query, the cleanest pattern is a small wrapper that uses your secret manager (here, `op run` from the 1Password CLI) to inject the token at exec time: + +```bash +mkdir -p ~/bin +cat > ~/bin/terraphim-agent-pa <<'SH' +#!/usr/bin/env bash +exec op run --account my.1password.com \ + --env-file=<(echo 'JMAP_ACCESS_TOKEN=op://VAULT/ITEM/credential') \ + -- /Users/alex/.cargo/bin/terraphim-agent "$@" +SH +chmod +x ~/bin/terraphim-agent-pa +``` + +Replace `VAULT/ITEM` with the path to your Fastmail token in 1Password. After this, `terraphim-agent-pa` behaves exactly like `terraphim-agent` for the Personal Assistant role; the bare `terraphim-agent` continues to work for the other roles without paying the 1Password unlock cost. + +The token only ever exists inside the running process. Verify nothing leaked to disk: + +```bash +grep -r "JMAP_ACCESS_TOKEN\|fmu1-" ~/.config/terraphim/ +``` + +The grep should return nothing. + +## Step 3 -- Verify search + +Notes-only query (no token needed; `terraphim-agent` is fine): + +```bash +terraphim-agent search --role "Personal Assistant" --limit 3 "todo" +``` + +Each hit should have a path under your Obsidian vault. + +Email query (use the wrapper): + +```bash +terraphim-agent-pa search --role "Personal Assistant" --limit 3 "invoice" +``` + +Each hit should have a `jmap:///email/` URL and the sender's address in the description. + +Cross-source query -- a term that appears in both notes and email, e.g. a project name: + +```bash +terraphim-agent-pa search --role "Personal Assistant" --limit 6 "" +``` + +You should see notes and emails interleaved, ordered by `terraphim-graph` rank. + +## Auto-routing + +When you call `terraphim-agent search "query"` without `--role`, the agent now scores every configured role's knowledge graph against the query and picks the highest-ranked match. The decision is printed once on stderr: + +``` +[auto-route] picked role "Personal Assistant" (score=2, candidates=4); to override, pass --role +``` + +stdout is untouched, so `--robot` and `--format json` output remain pure JSON. Pass `--role "Some Role"` to short-circuit auto-routing. + +When `JMAP_ACCESS_TOKEN` is not set, the Personal Assistant's score is multiplied by 0.5 (it loses the JMAP half of its corpus). The role still competes -- a clearly PA-flavoured query like `invoice tax` still wins over local-only roles when only PA matches. + +## Troubleshooting + +- **No email hits, no error** -- the warning `JMAP haystack support not enabled. Skipping haystack:` in stderr means your binary lacks the `jmap` feature. Rebuild from local source per the Prerequisites. +- **No email hits, no warning** -- run the wrapper with `op run --no-masking` once and confirm `JMAP_ACCESS_TOKEN` is non-empty inside the subshell. If empty, the 1Password reference is wrong. +- **`401 Unauthorized` from Fastmail** -- the token has been revoked or scoped without "Mail" access. Regenerate at . +- **Ranking feels off** -- the role's knowledge graph indexes the whole vault on first use; subsequent edits to notes need a `terraphim-agent config reload` (which rebuilds the role's KG within ~20 ms). +- **UTF-8 panic in CLI output** -- some snippets containing fancy quotes can trip a known truncation bug at `crates/terraphim_agent/src/main.rs:1414`. The search itself succeeds; only the trailing display crashes. Pipe through `head -n N` to bound the output until the upstream fix lands. + +## Related + +- [Command Rewriting How-To](../command-rewriting-howto.md) -- generic pattern for adding haystacks to any role. +- [Architecture](../Architecture.md) -- how roles, haystacks, and the knowledge graph compose. +- [Crates Overview](../crates-overview.md) -- where `haystack_jmap` and `terraphim_middleware` live. diff --git a/projects/odilo/data/odilo-etom-sale-processes.json b/projects/odilo/data/odilo-etom-sale-processes.json new file mode 100644 index 000000000..454848ac6 --- /dev/null +++ b/projects/odilo/data/odilo-etom-sale-processes.json @@ -0,0 +1,183 @@ +{ + "framework": "eTOM", + "version": "GB921-R21", + "pilot_scope": "DLT Pilot — Odilo x MTN", + "note": "Subset of CRM processes relevant to SALE skill. Excludes Problem Handling (1.A.1.6), Customer QoS/SLA Management (1.A.1.7), and Billing & Collections Management (1.B.1.8) — assurance and billing processes not applicable to SALE competencies.", + "processes": [ + { + "process_id": "1.FAB.1.2", + "level": 2, + "name": "Customer Interface Management", + "fab_category": "FAB", + "ancestor_path": ["Operations", "Customer Relationship Management"] + }, + { + "process_id": "1.FAB.1.2.1", + "level": 3, + "name": "Manage Contact", + "parent_id": "1.FAB.1.2", + "ancestor_path": ["Operations", "Customer Relationship Management", "Customer Interface Management"] + }, + { + "process_id": "1.FAB.1.2.2", + "level": 3, + "name": "Manage Request (Including Self Service)", + "parent_id": "1.FAB.1.2", + "ancestor_path": ["Operations", "Customer Relationship Management", "Customer Interface Management"] + }, + { + "process_id": "1.FAB.1.2.3", + "level": 3, + "name": "Analyse and Report on Customer", + "parent_id": "1.FAB.1.2", + "ancestor_path": ["Operations", "Customer Relationship Management", "Customer Interface Management"] + }, + { + "process_id": "1.F.1.3", + "level": 2, + "name": "Marketing Fulfilment Response", + "fab_category": "F", + "ancestor_path": ["Operations", "Customer Relationship Management"] + }, + { + "process_id": "1.F.1.3.1", + "level": 3, + "name": "Issue and Distribute Marketing Collaterals", + "parent_id": "1.F.1.3", + "ancestor_path": ["Operations", "Customer Relationship Management", "Marketing Fulfilment Response"] + }, + { + "process_id": "1.F.1.3.2", + "level": 3, + "name": "Track Leads", + "parent_id": "1.F.1.3", + "ancestor_path": ["Operations", "Customer Relationship Management", "Marketing Fulfilment Response"] + }, + { + "process_id": "1.F.1.4", + "level": 2, + "name": "Selling", + "fab_category": "F", + "ancestor_path": ["Operations", "Customer Relationship Management"] + }, + { + "process_id": "1.F.1.4.1", + "level": 3, + "name": "Manage Prospect", + "parent_id": "1.F.1.4", + "ancestor_path": ["Operations", "Customer Relationship Management", "Selling"] + }, + { + "process_id": "1.F.1.4.2", + "level": 3, + "name": "Qualify and Educate Customer", + "parent_id": "1.F.1.4", + "ancestor_path": ["Operations", "Customer Relationship Management", "Selling"] + }, + { + "process_id": "1.F.1.4.3", + "level": 3, + "name": "Negotiate Sales", + "parent_id": "1.F.1.4", + "ancestor_path": ["Operations", "Customer Relationship Management", "Selling"] + }, + { + "process_id": "1.F.1.4.4", + "level": 3, + "name": "Acquire Customer Data", + "parent_id": "1.F.1.4", + "ancestor_path": ["Operations", "Customer Relationship Management", "Selling"] + }, + { + "process_id": "1.F.1.4.5", + "level": 3, + "name": "Cross/Up Selling", + "parent_id": "1.F.1.4", + "ancestor_path": ["Operations", "Customer Relationship Management", "Selling"] + }, + { + "process_id": "1.F.1.5", + "level": 2, + "name": "Order Handling", + "fab_category": "F", + "ancestor_path": ["Operations", "Customer Relationship Management"] + }, + { + "process_id": "1.F.1.5.1", + "level": 3, + "name": "Determine PreOrder Feasibility", + "parent_id": "1.F.1.5", + "ancestor_path": ["Operations", "Customer Relationship Management", "Order Handling"] + }, + { + "process_id": "1.F.1.5.2", + "level": 3, + "name": "Authorize Credit", + "parent_id": "1.F.1.5", + "ancestor_path": ["Operations", "Customer Relationship Management", "Order Handling"] + }, + { + "process_id": "1.F.1.5.3", + "level": 3, + "name": "Receive PO and Issue Orders", + "parent_id": "1.F.1.5", + "ancestor_path": ["Operations", "Customer Relationship Management", "Order Handling"] + }, + { + "process_id": "1.F.1.5.4", + "level": 3, + "name": "Track Order & Manage Jeopardy", + "parent_id": "1.F.1.5", + "ancestor_path": ["Operations", "Customer Relationship Management", "Order Handling"] + }, + { + "process_id": "1.F.1.5.5", + "level": 3, + "name": "Complete Order", + "parent_id": "1.F.1.5", + "ancestor_path": ["Operations", "Customer Relationship Management", "Order Handling"] + }, + { + "process_id": "1.FAB.1.9", + "level": 2, + "name": "Retention & Loyalty", + "fab_category": "FAB", + "ancestor_path": ["Operations", "Customer Relationship Management"] + }, + { + "process_id": "1.FAB.1.9.1", + "level": 3, + "name": "Establish & Terminate Customer Relationship", + "parent_id": "1.FAB.1.9", + "ancestor_path": ["Operations", "Customer Relationship Management", "Retention & Loyalty"] + }, + { + "process_id": "1.FAB.1.9.2", + "level": 3, + "name": "Build Customer Insight", + "parent_id": "1.FAB.1.9", + "ancestor_path": ["Operations", "Customer Relationship Management", "Retention & Loyalty"] + }, + { + "process_id": "1.FAB.1.9.3", + "level": 3, + "name": "Analyse and Manage Customer Risk", + "parent_id": "1.FAB.1.9", + "ancestor_path": ["Operations", "Customer Relationship Management", "Retention & Loyalty"] + }, + { + "process_id": "1.FAB.1.9.4", + "level": 3, + "name": "Personalize Customer Profile for Retention & Loyalty", + "parent_id": "1.FAB.1.9", + "ancestor_path": ["Operations", "Customer Relationship Management", "Retention & Loyalty"] + }, + { + "process_id": "1.FAB.1.9.5", + "level": 3, + "name": "Validate Customer Satisfaction", + "parent_id": "1.FAB.1.9", + "ancestor_path": ["Operations", "Customer Relationship Management", "Retention & Loyalty"] + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/en/attributes.json b/projects/odilo/data/sfia-9-json/en/attributes.json new file mode 100644 index 000000000..752c53b52 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/en/attributes.json @@ -0,0 +1,743 @@ +{ + "type": "sfia.attributes", + "sfia_version": 9, + "language": "en", + "items": [ + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "AUTO", + "name": "Autonomy", + "url": "https://sfia-online.org/en/shortcode/9/AUTO", + "attribute_type": "Attributes", + "overall_description": "The level of independence, discretion and accountability for results in your role.", + "guidance_notes": "Autonomy in SFIA represents a progression from following instructions to setting organisational strategy. It involves:\n\nworking under varying levels of direction and supervision\n\nmaking independent decisions in line with responsibility\n\ntaking accountability for actions and their outcomes\n\ndelegating tasks and responsibilities appropriately\n\nsetting personal, team, or organisational goals.\n\nEffective autonomy encompasses decision-making skills, self-management and the ability to balance independence with organisational goals. Autonomy is closely linked with skills such as decision-making, leadership, and planning.\n\nAs professionals advance, their level of autonomy increasingly shapes their ability to drive change, innovate and contribute to organisational success. As professionals advance, their autonomy enables them to lead initiatives and drive strategic outcomes.  At higher levels, individuals shape their role and make decisions that have a wider organisational impact, with minimal supervision.", + "levels": [ + { + "level": 1, + "description": "Follows instructions and works under close direction. Receives specific instructions and guidance, has work closely reviewed." + }, + { + "level": 2, + "description": "Works under routine direction. Receives instructions and guidance, has work regularly reviewed." + }, + { + "level": 3, + "description": "Works under general direction to complete assigned tasks. Receives guidance and has work reviewed at agreed milestones. When required, delegates routine tasks to others within own team." + }, + { + "level": 4, + "description": "Works under general direction within a clear framework of accountability. Exercises considerable personal responsibility and autonomy. When required, plans, schedules, and delegates work to others, typically within own team." + }, + { + "level": 5, + "description": "Works under broad direction. Work is self-initiated, consistent with agreed operational and budgetary requirements for meeting allocated technical and/or group objectives. Defines tasks and delegates work to teams and individuals within area of responsibility." + }, + { + "level": 6, + "description": "Guides high level decisions and strategies within the organisation’s overall policies and objectives. Has defined authority and accountability for actions and decisions within a significant area of work, including technical, financial and quality aspects. Delegates responsibility for operational objectives." + }, + { + "level": 7, + "description": "Defines and leads the organisation’s vision and strategy within over-arching business objectives. Is fully accountable for actions taken and decisions made, both by self and others to whom responsibilities have been assigned. Delegates authority and responsibility for strategic business objectives." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "INFL", + "name": "Influence", + "url": "https://sfia-online.org/en/shortcode/9/INFL", + "attribute_type": "Attributes", + "overall_description": "The reach and impact of your decisions and actions, both within and outside the organisation.", + "guidance_notes": "Influence in SFIA reflects a progression from impacting immediate colleagues to shaping organisational direction. It involves:\n\nexpanding the sphere of interaction and impact\n\nprogressing from transactional to strategic interactions\n\nengaging with stakeholders, both internal and external, at increasing levels of seniority\n\nshaping decisions with growing organisational impact\n\ncontributing to team, departmental and organisational direction.\n\nInfluence is closely linked with other attributes such as, communication and leadership. Effective influence develops through experience and interaction with more senior levels of the organisation and industry. This attribute reflects the reach and impact of decisions and actions, both within and outside the organisation. \n\nAs professionals advance, their influence extends beyond their team, contributing to strategic decisions and helping shape the organisation’s direction. It progresses from awareness of how one's work supports others to directing strategy at an organisational level. The extent of influence is often reflected in the nature of interactions, the level of contacts and the impact of decisions on organisational direction.", + "levels": [ + { + "level": 1, + "description": "When required, contributes to team discussions with immediate colleagues." + }, + { + "level": 2, + "description": "Is expected to contribute to team discussions with immediate team members. Works alongside team members, contributing to team decisions. When the role requires, interacts with people outside their team, including internal colleagues and external contacts." + }, + { + "level": 3, + "description": "Works with and influences team decisions. Has a transactional level of contact with people outside their team, including internal colleagues and external contacts." + }, + { + "level": 4, + "description": "Influences projects and team objectives. Has a tactical level of contact with people outside their team, including internal colleagues and external contacts." + }, + { + "level": 5, + "description": "Influences critical decisions in their domain.  Has operational level contact impacting execution and implementation with internal colleagues and external contacts. Has significant influence over the allocation and management of resources required to deliver projects." + }, + { + "level": 6, + "description": "Influences the formation of strategy and the execution of business plans. Has a significant management level of contact with internal colleagues and external contacts. Has organisational leadership and influence over the appointment and management of resources related to the implementation of strategic initiatives." + }, + { + "level": 7, + "description": "Directs, influences and inspires the strategic direction and development of the organisation. Has an extensive leadership level of contact with internal colleagues and external contacts. Authorises the appointment of required resources." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "COMP", + "name": "Complexity", + "url": "https://sfia-online.org/en/shortcode/9/COMP", + "attribute_type": "Attributes", + "overall_description": "The range and intricacy of tasks and responsibilities that come with your role.", + "guidance_notes": "Complexity in SFIA represents a progression from routine tasks to strategic leadership delivering business value. It involves:\n\nhandling increasingly varied and unpredictable work environments\n\naddressing a growing range of technical or professional activities\n\nsolving progressively complex problems\n\nmanaging diverse stakeholders\n\ncontributing to policy and strategy\n\nleveraging emerging technologies for business value.\n\nEffective management of complexity encompasses skills in problem solving, decision making and planning alongside technical or professional expertise.  This attribute reflects the range and intricacy of tasks and responsibilities in a role, progressing from routine activities to extensive strategic leadership. It can be measured by the level of problem-solving required, the nature and number of stakeholders involved, and the impact of decisions made.\n\nAs professionals advance, their ability to navigate and leverage complexity increasingly contributes to organisational innovation, efficiency and competitive advantage.", + "levels": [ + { + "level": 1, + "description": "Performs routine activities in a structured environment." + }, + { + "level": 2, + "description": "Performs a range of work activities in varied environments." + }, + { + "level": 3, + "description": "Performs a range of work, sometimes complex and non-routine, in varied environments." + }, + { + "level": 4, + "description": "Work includes a broad range of complex technical or professional activities in varied contexts." + }, + { + "level": 5, + "description": "Performs an extensive range of complex technical and/or professional work activities, requiring the application of fundamental principles in a range of unpredictable contexts." + }, + { + "level": 6, + "description": "Performs highly complex work activities covering technical, financial and quality aspects." + }, + { + "level": 7, + "description": "Performs extensive strategic leadership in delivering business value through vision, governance and executive management." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "KNGE", + "name": "Knowledge", + "url": "https://sfia-online.org/en/shortcode/9/KNGE", + "attribute_type": "Attributes", + "overall_description": "The depth and breadth of understanding required to perform and influence work effectively.", + "guidance_notes": "Knowledge in SFIA represents a progression from applying basic role-specific information to leveraging broad, strategic understanding that shapes organisational direction and industry trends. It involves:\n\napplying role-specific knowledge to perform routine tasks\n\nintegrating general, role-specific, and industry knowledge\n\nusing understanding of technologies, methods, and processes to achieve results\n\napplying in-depth expertise to solve complex problems\n\nleveraging broad knowledge to influence strategic decisions\n\nshaping organisational knowledge management practices.\n\nEffective knowledge application develops through practical experience, formal education, professional training, continuous learning, and mentorship. It encompasses the ability to apply understanding in real-world scenarios, adapt to emerging challenges, and create value for the organisation.\n\nAs professionals advance, their application of knowledge evolves significantly from basic, role-specific tasks to strategic organisational leadership. This progression involves supporting team activities, applying practices within business contexts, integrating knowledge for complex tasks, offering authoritative advice, and enabling cross-domain decision-making. At higher levels, professionals apply broad business and strategic knowledge to shape organisational strategy and anticipate industry trends.", + "levels": [ + { + "level": 1, + "description": "Applies basic knowledge to perform routine, well-defined, predictable role-specific tasks." + }, + { + "level": 2, + "description": "Applies knowledge of common workplace tasks and practices to support team activities under guidance." + }, + { + "level": 3, + "description": "Applies knowledge of a range of role-specific practices to complete tasks within defined boundaries and has an appreciation of how this knowledge applies to the wider business context." + }, + { + "level": 4, + "description": "Applies knowledge across different areas in their field, integrating this knowledge to perform complex and diverse tasks. Applies a working knowledge of the organisation’s domain." + }, + { + "level": 5, + "description": "Applies knowledge to interpret complex situations and offer authoritative advice. Applies in-depth expertise in specific fields, with a broader understanding across industry/business." + }, + { + "level": 6, + "description": "Applies broad business knowledge to enable strategic leadership and decision-making across various domains." + }, + { + "level": 7, + "description": "Applies strategic and broad-based knowledge to shape organisational strategy, anticipate future industry trends, and prepare the organisation to adapt and lead." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "COLL", + "name": "Collaboration", + "url": "https://sfia-online.org/en/shortcode/9/COLL", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Working effectively with others, sharing resources and coordinating efforts to achieve shared objectives.", + "guidance_notes": "Collaboration in SFIA represents a progression from basic team interaction to strategic partnerships and stakeholder management. It involves:\n\nworking cooperatively within immediate teams\n\nsharing information and resources effectively\n\ncoordinating efforts to achieve common goals\n\nfacilitating cross-functional teamwork\n\nbuilding influential relationships across the organisation\n\nestablishing and managing strategic partnerships.\n\nEffective collaboration encompasses communication, perspective-taking, and the ability to align diverse viewpoints towards common objectives. It also involves creating an environment that encourages knowledge sharing and collective problem-solving.\n\nAs professionals advance, their collaborative skills evolve from supporting team goals to shaping organisational culture, driving innovation, and enhancing the organisation's ability to navigate complex challenges. At higher levels, collaboration extends to influencing industry-wide cooperation and partnerships.", + "levels": [ + { + "level": 1, + "description": "Works mostly on their own tasks and interacts with their immediate team only. Develops an understanding of how their work supports others." + }, + { + "level": 2, + "description": "Understands the need to collaborate with their team and considers user/customer needs." + }, + { + "level": 3, + "description": "Understands and collaborates on the analysis of user/customer needs and represents this in their work." + }, + { + "level": 4, + "description": "Facilitates collaboration between stakeholders who share common objectives.  \n\nEngages with and contributes to the work of cross-functional teams to ensure that user/customer needs are being met throughout the deliverable/scope of work." + }, + { + "level": 5, + "description": "Facilitates collaboration between stakeholders who have diverse objectives.\n\nEnsures collaborative ways of working throughout all stages of work to meet user/customer needs.\n\nBuilds effective relationships across the organisation and with customers, suppliers and partners." + }, + { + "level": 6, + "description": "Leads collaboration with a diverse range of stakeholders across competing objectives within the organisation.\n\nBuilds strong, influential connections with key internal and external contacts at senior management/technical leader level" + }, + { + "level": 7, + "description": "Drives collaboration, engaging with leadership stakeholders ensuring alignment to corporate vision and strategy. \n\nBuilds strong, influential relationships with customers, partners and industry leaders." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "COMM", + "name": "Communication", + "url": "https://sfia-online.org/en/shortcode/9/COMM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Exchanging information, ideas and insights clearly to enable mutual understanding and cooperation.", + "guidance_notes": "Communication in SFIA represents a progression from basic team interaction to complex, organisation-wide influence and external engagement. It involves:\n\ncommunicating within immediate teams\n\nexchanging information and ideas clearly\n\nverbal and written skills, active listening, and the ability to use communication tools and platforms appropriately\n\nadapting communication style to diverse audiences, both technical and non-technical\n\narticulating complex concepts in a way that enables informed decision-making\n\ninfluencing strategy through effective dialogue with senior stakeholders.\n\nAs professionals advance, their communication skills evolve from simple information sharing within teams to influencing decisions at the highest levels of an organisation. This progression involves adapting communication to different audiences, including senior stakeholders and external partners, and shaping strategic outcomes through effective dialogue. At higher levels, professionals take on the responsibility of using communication to drive organisational direction and engage with industry leaders to achieve business objectives.", + "levels": [ + { + "level": 1, + "description": "Communicates with immediate team to understand and deliver on their assigned tasks. Observes, listens, and with encouragement, asks questions to seek information or clarify instructions." + }, + { + "level": 2, + "description": "Communicates familiar information with immediate team and stakeholders directly related to their role.\n\nListens to gain understanding and asks relevant questions to clarify or seek further information." + }, + { + "level": 3, + "description": "Communicates with team and stakeholders inside and outside the organisation clearly explaining and presenting information.\n\nContributes to a range of work-related conversations and listens to others to gain an understanding and asks probing questions relevant to their role." + }, + { + "level": 4, + "description": "Communicates with both technical and non-technical audiences including team and stakeholders inside and outside the organisation.\n\nAs required, takes the lead in explaining complex concepts to support decision making.\n\nListens and asks insightful questions to identify different perspectives to clarify and confirm understanding." + }, + { + "level": 5, + "description": "Communicates clearly with impact, articulating complex information and ideas to broad audiences with different viewpoints.\n\nLeads and encourages conversations to share ideas and build consensus on actions to be taken." + }, + { + "level": 6, + "description": "Communicates with credibility at all levels across the organisation to broad audiences with divergent objectives.\n\nExplains complex information and ideas clearly, influencing the strategic direction.\n\nPromotes information sharing across the organisation." + }, + { + "level": 7, + "description": "Communicates to audiences at all levels within own organisation and engages with industry.\n\nPresents compelling arguments and ideas authoritatively and convincingly to achieve business objectives." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "IMPM", + "name": "Improvement mindset", + "url": "https://sfia-online.org/en/shortcode/9/IMPM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Continuously identifying opportunities to refine work practices, processes, products, or services for greater efficiency and impact.", + "guidance_notes": "Having an improvement mindset in SFIA represents a progression from recognising opportunities for enhancement to driving a culture of ongoing optimisation. It involves:\n\nidentifying areas for improvement in processes, products, or services\n\nimplementing changes to enhance efficiency and effectiveness\n\nassessing the impact of improvements and refining approaches\n\nencouraging and supporting a mindset of continuous improvement in others\n\naligning improvement initiatives with organisational objectives\n\ncultivating a culture of ongoing enhancement and optimisation\n\nAn improvement mindset involves proactively seeking opportunities to refine and optimise work practices, processes, products, and services. This reflects the growing responsibility to identify, implement, and lead improvements across increasing scopes of influence.As professionals advance, their focus shifts from identifying opportunities for improvement in their own tasks to leading improvement initiatives across teams and the organisation. This progression includes enhancing practices at a personal level, supporting others in promoting a culture of continuous optimisation, and ensuring improvement efforts align with broader organisational goals. At higher levels, professionals take responsibility for embedding ongoing improvement strategies throughout the organisation, driving long-term impact.", + "levels": [ + { + "level": 1, + "description": "Identifies opportunities for improvement in own tasks. Suggests basic enhancements when prompted." + }, + { + "level": 2, + "description": "Proposes ideas to improve own work area.\n\nImplements agreed changes to assigned work tasks." + }, + { + "level": 3, + "description": "Identifies and implements improvements in own work area.\n\nContributes to team-level process enhancements." + }, + { + "level": 4, + "description": "Encourages and supports team discussions on improvement initiatives.\n\nImplements procedural changes within a defined scope of work." + }, + { + "level": 5, + "description": "Identifies and evaluates potential improvements to products, practices, or services.\n\nLeads implementation of enhancements within own area of responsibility.\n\nAssesses effectiveness of implemented changes." + }, + { + "level": 6, + "description": "Drives improvement initiatives that have a significant impact on the organisation.\n\nAligns improvement strategies with organisational objectives.\n\nEngages stakeholders in improvement processes." + }, + { + "level": 7, + "description": "Defines and communicates the organisational approach to continuous improvement.\n\nCultivates a culture of ongoing enhancement.\n\nEvaluates the impact of improvement initiatives on organisational success." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "CRTY", + "name": "Creativity", + "url": "https://sfia-online.org/en/shortcode/9/CRTY", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Generating and applying innovative ideas to enhance processes, solve problems and drive organisational success.", + "guidance_notes": "Creativity in SFIA represents a progression from basic idea generation to driving strategic innovation. It involves:\n\ngenerating novel ideas and solutions\n\napplying innovative thinking to improve processes\n\nsolving complex problems creatively\n\nencouraging and facilitating creative thinking in others\n\ndeveloping a culture of innovation\n\naligning creative initiatives with organisational strategy.\n\nEffective creativity encompasses imaginative thinking, problem-solving skills and challenging conventional approaches. It thrives in environments that encourage calculated risk-taking and value innovative ideas.\n\nAs professionals advance, their role shifts from contributing to creative processes to inspiring and leading innovation at a strategic level. This evolution underscores the growing importance of creative thinking in driving organisational success and navigating complex challenges across various disciplines.", + "levels": [ + { + "level": 1, + "description": "Participates in the generation of new ideas when prompted." + }, + { + "level": 2, + "description": "Applies creative thinking to suggest new ways to approach a task and solve problems." + }, + { + "level": 3, + "description": "Applies and contributes to creative thinking techniques to contribute new ideas for their own work and for team activities." + }, + { + "level": 4, + "description": "Applies, facilitates and develops creative thinking concepts and finds alternative ways to approach team outcomes." + }, + { + "level": 5, + "description": "Creatively applies innovative thinking and design practices in identifying solutions that will deliver value for the benefit of the customer/stakeholder." + }, + { + "level": 6, + "description": "Creatively applies a wide range of new ideas and effective management techniques to achieve results that align with organisational strategy." + }, + { + "level": 7, + "description": "Champions creativity and innovation in driving strategy development to enable business opportunities." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "DECM", + "name": "Decision-making", + "url": "https://sfia-online.org/en/shortcode/9/DECM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Applying critical thinking to evaluate options, assess risks and select the most appropriate course of action.", + "guidance_notes": "Decision making in SFIA represents a progression from routine choices to strategic, high-impact decisions. It involves:\n\nevaluating information and assessing risks\n\nbalancing intuition and logic\n\nunderstanding organisational context\n\ndetermining the best course of action\n\ntaking accountability for outcomes.\n\nEffective decision making encompasses analytical and critical thinking skills, the ability to assess risks and consequences, and a comprehensive understanding of business context. It also involves knowing when to escalate issues and how to balance competing priorities.\n\nAs professionals advance, their decision-making evolves from addressing routine issues to shaping strategic directions. Early on, decisions focus on managing tasks or small projects. Over time, decision-making becomes more complex, requiring greater judgement, risk assessment, and accountability for high-impact outcomes. At higher levels, professionals are responsible for making critical decisions that influence organisational strategy and success.", + "levels": [ + { + "level": 1, + "description": "Uses little discretion in attending to enquiries.  \n\nIs expected to seek guidance in unexpected situations." + }, + { + "level": 2, + "description": "Uses limited discretion in resolving issues or enquiries.\n\nDecides when to seek guidance in unexpected situations." + }, + { + "level": 3, + "description": "Uses discretion in identifying and responding to complex issues related to own assignments. \n\nDetermines when issues should be escalated to a higher level." + }, + { + "level": 4, + "description": "Uses judgment and substantial discretion in identifying and responding to complex issues and assignments related to projects and team objectives.\n\nEscalates when scope is impacted." + }, + { + "level": 5, + "description": "Uses judgement to make informed decisions on actions to achieve organisational outcomes such as meeting targets, deadlines, and budget.\n\nRaises issues when objectives are at risk." + }, + { + "level": 6, + "description": "Uses judgement to make decisions that initiate the achievement of agreed strategic objectives including financial performance.\n\nEscalates when broader strategic direction is impacted." + }, + { + "level": 7, + "description": "Uses judgement in making decisions critical to the organisational strategic direction and success.\n\nEscalates when business executive management input is required through established governance structures." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "DIGI", + "name": "Digital mindset", + "url": "https://sfia-online.org/en/shortcode/9/DIGI", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Embracing and effectively using digital tools and technologies to enhance performance and productivity.", + "guidance_notes": "Having a digital mindset in SFIA represents a progression from basic digital literacy to driving organisational digital strategy. It involves:\n\nunderstanding and applying digital technologies\n\nadapting to rapidly evolving digital landscapes\n\nusing digital tools, AI and data to enhance work processes\n\ndriving digital innovation and transformation\n\nunderstanding the implications of emerging technologies, including AI, and their potential to drive organisational change\n\nensuring digital governance and compliance.\n\nAn effective digital mindset encompasses continuous learning, adaptability and the ability to see how digital technologies can transform business models and strategies. It also involves understanding the implications of emerging technologies and their potential to drive organisational change.\n\nAs professionals advance, their digital mindset evolves from simply using digital tools to shaping and leading organisational digital strategies. Early in their careers, they focus on applying digital skills to their roles, but as they progress, they begin driving innovation and using emerging technologies to transform work processes. At higher levels, professionals are responsible for leading digital transformation, ensuring compliance with digital governance, and embedding a digital culture across the organisation.", + "levels": [ + { + "level": 1, + "description": "Has basic digital skills to learn and use applications, processes and tools for their role." + }, + { + "level": 2, + "description": "Has sufficient digital skills for their role; understands and uses appropriate methods, tools, applications and processes." + }, + { + "level": 3, + "description": "Explores and applies relevant digital tools and skills for their role.\n\nUnderstands and effectively applies appropriate methods, tools, applications and processes." + }, + { + "level": 4, + "description": "Maximises the capabilities of applications for their role and evaluates and supports the use of new technologies and digital tools.\n\nSelects appropriately from, and assesses the impact of change to applicable standards, methods, tools, applications and processes relevant to own specialism." + }, + { + "level": 5, + "description": "Recognises and evaluates the organisational impact of new technologies and digital services.\n\nImplements new and effective practices. \n\nAdvises on available standards, methods, tools, applications and processes relevant to group specialism(s) and can make appropriate choices from alternatives." + }, + { + "level": 6, + "description": "Leads the enhancement of the organisation’s digital capabilities. \n\nIdentifies and endorses opportunities to adopt new technologies and digital services.\n\nLeads digital governance and compliance with relevant legislation and the need for products and services." + }, + { + "level": 7, + "description": "Leads the development of the organisation’s digital culture and the transformational vision.  \n\nAdvances capability and/or exploitation of technology within one or more organisations through a deep understanding of the industry and the implications of emerging technologies.\n\nAccountable for assessing how laws and regulations impact organisational objectives and its use of digital, data and technology capabilities." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "LEAD", + "name": "Leadership", + "url": "https://sfia-online.org/en/shortcode/9/LEAD", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Guiding and influencing individuals or teams to align actions with strategic goals and drive positive outcomes.", + "guidance_notes": "Leadership in SFIA represents a progression from self-management to shaping organisational strategy. It involves:\n\ndemonstrating personal responsibility\n\ntaking ownership of work and development\n\nguiding and influencing others\n\ncontributing to team capabilities\n\naligning actions with organisational objectives\n\ninspiring and driving positive change.\n\nEffective leadership encompasses self-awareness, influence, understanding and inspiring and motivating others. It also involves strategic thinking, risk management and the capacity to align actions with long-term objectives.\n\nAs professionals advance, their leadership evolves from managing personal responsibilities to guiding teams and eventually shaping organisational strategy. Over time, they move beyond influencing teams to driving strategic outcomes, aligning policies with organisational goals, and managing risks on a broader scale. At higher levels, leadership plays a critical role in shaping organisational culture, driving innovation, and enhancing the organisation's ability to navigate complex challenges and seize opportunities.", + "levels": [ + { + "level": 1, + "description": "Proactively increases their understanding of their work tasks and responsibilities." + }, + { + "level": 2, + "description": "Takes ownership to develop their work experience." + }, + { + "level": 3, + "description": "Provides basic guidance and support to less experienced team members as needed." + }, + { + "level": 4, + "description": "Leads, supports or guides team members.\n\nDevelops solutions for complex work activities related to assignments. \n\nDemonstrates an understanding of risk factors in their work.\n\nContributes specialist expertise to requirements definition in support of proposals." + }, + { + "level": 5, + "description": "Provides leadership at an operational level.\n\nImplements and executes policies aligned to strategic plans.\n\nAssesses and evaluates risk.\n\nTakes all requirements into account when considering proposals." + }, + { + "level": 6, + "description": "Provides leadership at an organisational level.\n\nContributes to the development and implementation of policy and strategy.\n\nUnderstands and communicates industry developments, and the role and impact of technology. \n\nManages and mitigates organisational risk.  \n\nBalances the requirements of proposals with the broader needs of the organisation." + }, + { + "level": 7, + "description": "Leads strategic management.\n\nApplies the highest level of leadership to the formulation and implementation of strategy.\n\nCommunicates the potential impact of emerging practices and technologies on organisations and individuals and assesses the risks of using or not using such practices and technologies. \n\nEstablishes governance to address business risk.\n\nEnsures proposals align with the strategic direction of the organisation." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "LADV", + "name": "Learning and development", + "url": "https://sfia-online.org/en/shortcode/9/LADV", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Continuously acquiring new knowledge and skills to enhance personal and organisational performance.", + "guidance_notes": "Learning and professional development in SFIA represents a progression from personal skill enhancement to shaping organisational learning culture. It involves:\n\nacquiring and applying new knowledge\n\nidentifying and addressing skill gaps\n\nsharing learnings with colleagues\n\ndriving personal and team development\n\npromoting knowledge application for strategic goals\n\ninspiring a learning culture aligned with business objectives.\n\nEffective learning and professional development encompass formal education, experiential learning, self-directed study and the ability to critically assess and apply new information. It also involves maintaining awareness of emerging practices and industry trends, and aligning learning initiatives with strategic business objectives.As professionals advance, their approach to learning and development evolves from focusing on personal skill enhancement to driving team and organisational development. Over time, they move from applying new knowledge to leading efforts that shape a culture of learning, aligning development initiatives with strategic goals. At senior levels, professionals not only inspire a learning culture but also ensure that the organisation has the necessary skills and capabilities to navigate industry changes and take advantage of opportunities.", + "levels": [ + { + "level": 1, + "description": "Applies newly acquired knowledge to develop  skills for their role. Contributes to identifying own development opportunities." + }, + { + "level": 2, + "description": "Absorbs and applies new information to tasks.\n\nRecognises personal skills and knowledge gaps and seeks learning opportunities to address them." + }, + { + "level": 3, + "description": "Absorbs and applies new information effectively with the ability to share learnings with colleagues.\n\nTakes the initiative in identifying and negotiating their own appropriate development opportunities." + }, + { + "level": 4, + "description": "Rapidly absorbs and critically assesses new information and applies it effectively.\n\nMaintains an understanding of emerging practices and their application and takes responsibility for driving own and team members’ development opportunities." + }, + { + "level": 5, + "description": "Uses their skills and knowledge to help establish the standards that others in the organisation will apply.\n\nTakes the initiative to develop a wider breadth of knowledge across industry and/or business and identify and manage development opportunities in area of responsibility." + }, + { + "level": 6, + "description": "Promotes the application of knowledge to support strategic imperatives.\n\nActively develops their strategic and technical leadership skills and leads the development of skills in their area of accountability." + }, + { + "level": 7, + "description": "Inspires a learning culture to align with business objectives.   \n\nMaintains strategic insight into contemporary and emerging industry landscapes. \n\nEnsures the organisation develops and mobilises the full range of required skills and capabilities." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "PLAN", + "name": "Planning", + "url": "https://sfia-online.org/en/shortcode/9/PLAN", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Taking a systematic approach to organising tasks, resources and timelines to meet defined goals.", + "guidance_notes": "Planning in SFIA represents a progression from organising individual work to leading strategic planning across an organisation. It involves: \n\nsetting objectives and determining timelines\n\norganising tasks and allocating resources\n\naligning activities with larger goals\n\nadapting plans to changing circumstances\n\nmonitoring progress and evaluating outcomes\n\ninitiating and influencing strategic objectives.\n\nEffective planning encompasses analytical skills, foresight and the ability to balance multiple priorities. It also involves adaptability to respond to changing circumstances, and the capacity to align operational plans with strategic goals. As professionals advance, their planning skills increasingly shape organisational direction and performance.\n\nAs professionals advance, their planning responsibilities grow from managing personal or team tasks to driving organisational planning efforts. Over time, they progress from organising their own work to setting strategic objectives that shape the direction of the organisation. At higher levels, professionals take the lead in planning complex initiatives, ensuring alignment with strategic goals, and guiding organisational performance.", + "levels": [ + { + "level": 1, + "description": "Confirms required steps for individual tasks." + }, + { + "level": 2, + "description": "Plans own work within short time horizons in an organised way." + }, + { + "level": 3, + "description": "Organises and keeps track of own work (and others where needed) to meet agreed timescales." + }, + { + "level": 4, + "description": "Plans, schedules and monitors work to meet given personal and/or team objectives and processes, demonstrating an analytical approach to meet time and quality targets." + }, + { + "level": 5, + "description": "Analyses, designs, plans, establishes milestones, and executes and evaluates work to time, cost and quality targets." + }, + { + "level": 6, + "description": "Initiates and influences strategic objectives and assigns responsibilities." + }, + { + "level": 7, + "description": "Plans and leads at the highest level of authority over all aspects of a significant area of work." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "PROB", + "name": "Problem-solving", + "url": "https://sfia-online.org/en/shortcode/9/PROB", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Analysing challenges, applying logical methods and developing effective solutions to overcome obstacles.", + "guidance_notes": "Problem solving in SFIA represents a progression from addressing routine issues to managing strategic challenges. It involves:\n\nrecognising and understanding problems\n\nanalysing potential solutions\n\nimplementing effective resolutions\n\nevaluating outcomes and learning from experiences\n\nanticipating and addressing potential issues proactively\n\naligning problem solving with organisational objectives.\n\nEffective problem solving encompasses analytical thinking, creativity and the ability to make informed decisions. It also involves collaboration with experts from various disciplines, particularly at senior levels.\n\nAs professionals advance, their problem-solving responsibilities grow from resolving routine issues to tackling complex, strategic challenges. Initially, they focus on methodical approaches to everyday problems, but over time, they develop the capacity to anticipate issues, evaluate a range of solutions, and address challenges that impact broader organisational objectives. At higher levels, professionals lead problem-solving efforts, ensuring that complex challenges are managed in alignment with long-term goals.", + "levels": [ + { + "level": 1, + "description": "Works towards understanding the issue and seeks assistance in resolving unexpected problems." + }, + { + "level": 2, + "description": "Investigates and resolves routine issues." + }, + { + "level": 3, + "description": "Applies a methodical approach to investigate and evaluate options to resolve routine and moderately complex issues." + }, + { + "level": 4, + "description": "Investigates the cause and impact, evaluates options and resolves a broad range of complex issues." + }, + { + "level": 5, + "description": "Investigates complex issues to identify the root causes and impacts, assesses a range of solutions, and makes informed decisions on the best course of action, often in collaboration with other experts." + }, + { + "level": 6, + "description": "Anticipates and leads in addressing problems and opportunities that may impact organisational objectives, establishing a strategic approach and allocating resources." + }, + { + "level": 7, + "description": "Manages inter-relationships between impacted parties and strategic imperatives, recognising the broader business context and drawing accurate conclusions when resolving problems." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "ADAP", + "name": "Adaptability", + "url": "https://sfia-online.org/en/shortcode/9/ADAP", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Adjusting to change and persisting through challenges at personal, team and organisational levels.", + "guidance_notes": "Adaptability and resilience in SFIA represent a progression from personal flexibility to shaping organisational agility. It involves:\n\nbeing open to change and new ways of working\n\nadjusting to different team dynamics and work requirements\n\nadopting new methods and technologies proactively\n\nenabling others to adapt to challenges\n\nleading teams through transitions\n\ndriving significant organisational changes\n\nembedding adaptability into organisational culture.\n\nEffective adaptability and resilience encompass openness to change, proactive learning and the ability to maintain focus on objectives during transitions. It also involves supporting others through change and creating an environment where innovation and flexibility thrive.\n\nAs professionals advance, their ability to drive and manage change increasingly shapes organisational resilience and long-term success in dynamic environments.", + "levels": [ + { + "level": 1, + "description": "Accepts change and is open to new ways of working." + }, + { + "level": 2, + "description": "Adjusts to different team dynamics and work requirements.\n\nParticipates in team adaptation processes." + }, + { + "level": 3, + "description": "Adapts and is responsive to change and shows initiative in adopting new methods or technologies." + }, + { + "level": 4, + "description": "Enables others to adapt and change in response to challenges and changes in the work environment." + }, + { + "level": 5, + "description": "Leads adaptations to changing business environments.\n\nGuides teams through transitions, maintaining focus on organisational objectives." + }, + { + "level": 6, + "description": "Drives organisational adaptability by initiating and leading significant changes. Influences change management strategies at an organisational level." + }, + { + "level": 7, + "description": "Champions organisational agility and resilience.\n\nEmbeds adaptability into organisational culture and strategic planning." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "en", + "code": "SCPE", + "name": "Security, privacy and ethics", + "url": "https://sfia-online.org/en/shortcode/9/SCPE", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Ensuring the protection of sensitive information, upholding privacy of data and individuals, and demonstrating ethical conduct within and outside the organisation.", + "guidance_notes": "Security, privacy and ethics in SFIA represent a progression from basic awareness to strategic leadership. It involves:\n\napplying professional working practices and adhering to organisational rules\n\nimplementing standards and best practices\n\npromoting a culture of security, privacy and ethical conduct\n\naddressing ethical challenges, including those introduced by emerging technologies, such as AI\n\nensuring compliance with relevant laws and regulations\n\nleading initiatives that embed security, privacy, and ethics into organisational culture and operations.\n\nEffective management of security, privacy and ethics encompasses technical knowledge, ethical decision-making skills and the ability to balance competing priorities. It also involves creating an environment where these principles are embedded in all aspects of work.\n\nAs professionals advance, they are expected to take an active role in promoting ethical behaviour and securing sensitive information across all areas of work. At higher levels, individuals are responsible for developing strategies that balance operational needs with ethical considerations, ensuring long-term sustainability and trust.", + "levels": [ + { + "level": 1, + "description": "Develops an understanding of the rules and expectations of their role and the organisation." + }, + { + "level": 2, + "description": "Has a good understanding of their role and the organisation’s rules and expectations." + }, + { + "level": 3, + "description": "Applies appropriate professionalism and working practices and knowledge to work." + }, + { + "level": 4, + "description": "Adapts and applies applicable standards, recognising their importance in achieving team outcomes." + }, + { + "level": 5, + "description": "Contributes proactively to the implementation of professional working practices and helps promote a supportive organisational culture." + }, + { + "level": 6, + "description": "Takes a leading role in promoting and ensuring appropriate culture and working practices, including the provision of equal access and opportunity to people with diverse abilities." + }, + { + "level": 7, + "description": "Provides clear direction and strategic leadership for embedding compliance, organisational culture, and working practices, and actively promotes diversity and inclusivity." + } + ], + "source": { + "file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "sheet": "Attributes", + "source_date": null + } + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/en/behaviour-matrix.json b/projects/odilo/data/sfia-9-json/en/behaviour-matrix.json new file mode 100644 index 000000000..61e9cd935 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/en/behaviour-matrix.json @@ -0,0 +1,895 @@ +{ + "type": "sfia.behaviour_matrix", + "sfia_version": 9, + "language": "en", + "factors": [ + { + "code": "COLL", + "name": "Collaboration", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration", + "levels": [ + { + "level": 1, + "description": "Works mostly on their own tasks and interacts with their immediate team only. Develops an understanding of how their work supports others." + }, + { + "level": 2, + "description": "Understands the need to collaborate with their team and considers user/customer needs." + }, + { + "level": 3, + "description": "Understands and collaborates on the analysis of user/customer needs and represents this in their work." + }, + { + "level": 4, + "description": "Facilitates collaboration between stakeholders who share common objectives.  \n\nEngages with and contributes to the work of cross-functional teams to ensure that user/customer needs are being met throughout the deliverable/scope of work." + }, + { + "level": 5, + "description": "Facilitates collaboration between stakeholders who have diverse objectives.\n\nEnsures collaborative ways of working throughout all stages of work to meet user/customer needs.\n\nBuilds effective relationships across the organisation and with customers, suppliers and partners." + }, + { + "level": 6, + "description": "Leads collaboration with a diverse range of stakeholders across competing objectives within the organisation.\n\nBuilds strong, influential connections with key internal and external contacts at senior management/technical leader level" + }, + { + "level": 7, + "description": "Drives collaboration, engaging with leadership stakeholders ensuring alignment to corporate vision and strategy. \n\nBuilds strong, influential relationships with customers, partners and industry leaders." + } + ] + }, + { + "code": "COMM", + "name": "Communication", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication", + "levels": [ + { + "level": 1, + "description": "Communicates with immediate team to understand and deliver on their assigned tasks. Observes, listens, and with encouragement, asks questions to seek information or clarify instructions." + }, + { + "level": 2, + "description": "Communicates familiar information with immediate team and stakeholders directly related to their role.\n\nListens to gain understanding and asks relevant questions to clarify or seek further information." + }, + { + "level": 3, + "description": "Communicates with team and stakeholders inside and outside the organisation clearly explaining and presenting information.\n\nContributes to a range of work-related conversations and listens to others to gain an understanding and asks probing questions relevant to their role." + }, + { + "level": 4, + "description": "Communicates with both technical and non-technical audiences including team and stakeholders inside and outside the organisation.\n\nAs required, takes the lead in explaining complex concepts to support decision making.\n\nListens and asks insightful questions to identify different perspectives to clarify and confirm understanding." + }, + { + "level": 5, + "description": "Communicates clearly with impact, articulating complex information and ideas to broad audiences with different viewpoints.\n\nLeads and encourages conversations to share ideas and build consensus on actions to be taken." + }, + { + "level": 6, + "description": "Communicates with credibility at all levels across the organisation to broad audiences with divergent objectives.\n\nExplains complex information and ideas clearly, influencing the strategic direction.\n\nPromotes information sharing across the organisation." + }, + { + "level": 7, + "description": "Communicates to audiences at all levels within own organisation and engages with industry.\n\nPresents compelling arguments and ideas authoritatively and convincingly to achieve business objectives." + } + ] + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement", + "levels": [ + { + "level": 1, + "description": "Identifies opportunities for improvement in own tasks. Suggests basic enhancements when prompted." + }, + { + "level": 2, + "description": "Proposes ideas to improve own work area.\n\nImplements agreed changes to assigned work tasks." + }, + { + "level": 3, + "description": "Identifies and implements improvements in own work area.\n\nContributes to team-level process enhancements." + }, + { + "level": 4, + "description": "Encourages and supports team discussions on improvement initiatives.\n\nImplements procedural changes within a defined scope of work." + }, + { + "level": 5, + "description": "Identifies and evaluates potential improvements to products, practices, or services.\n\nLeads implementation of enhancements within own area of responsibility.\n\nAssesses effectiveness of implemented changes." + }, + { + "level": 6, + "description": "Drives improvement initiatives that have a significant impact on the organisation.\n\nAligns improvement strategies with organisational objectives.\n\nEngages stakeholders in improvement processes." + }, + { + "level": 7, + "description": "Defines and communicates the organisational approach to continuous improvement.\n\nCultivates a culture of ongoing enhancement.\n\nEvaluates the impact of improvement initiatives on organisational success." + } + ] + }, + { + "code": "CRTY", + "name": "Creativity", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity", + "levels": [ + { + "level": 1, + "description": "Participates in the generation of new ideas when prompted." + }, + { + "level": 2, + "description": "Applies creative thinking to suggest new ways to approach a task and solve problems." + }, + { + "level": 3, + "description": "Applies and contributes to creative thinking techniques to contribute new ideas for their own work and for team activities." + }, + { + "level": 4, + "description": "Applies, facilitates and develops creative thinking concepts and finds alternative ways to approach team outcomes." + }, + { + "level": 5, + "description": "Creatively applies innovative thinking and design practices in identifying solutions that will deliver value for the benefit of the customer/stakeholder." + }, + { + "level": 6, + "description": "Creatively applies a wide range of new ideas and effective management techniques to achieve results that align with organisational strategy." + }, + { + "level": 7, + "description": "Champions creativity and innovation in driving strategy development to enable business opportunities." + } + ] + }, + { + "code": "DECM", + "name": "Decision-making", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision", + "levels": [ + { + "level": 1, + "description": "Uses little discretion in attending to enquiries.  \n\nIs expected to seek guidance in unexpected situations." + }, + { + "level": 2, + "description": "Uses limited discretion in resolving issues or enquiries.\n\nDecides when to seek guidance in unexpected situations." + }, + { + "level": 3, + "description": "Uses discretion in identifying and responding to complex issues related to own assignments. \n\nDetermines when issues should be escalated to a higher level." + }, + { + "level": 4, + "description": "Uses judgment and substantial discretion in identifying and responding to complex issues and assignments related to projects and team objectives.\n\nEscalates when scope is impacted." + }, + { + "level": 5, + "description": "Uses judgement to make informed decisions on actions to achieve organisational outcomes such as meeting targets, deadlines, and budget.\n\nRaises issues when objectives are at risk." + }, + { + "level": 6, + "description": "Uses judgement to make decisions that initiate the achievement of agreed strategic objectives including financial performance.\n\nEscalates when broader strategic direction is impacted." + }, + { + "level": 7, + "description": "Uses judgement in making decisions critical to the organisational strategic direction and success.\n\nEscalates when business executive management input is required through established governance structures." + } + ] + }, + { + "code": "DIGI", + "name": "Digital mindset", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset", + "levels": [ + { + "level": 1, + "description": "Has basic digital skills to learn and use applications, processes and tools for their role." + }, + { + "level": 2, + "description": "Has sufficient digital skills for their role; understands and uses appropriate methods, tools, applications and processes." + }, + { + "level": 3, + "description": "Explores and applies relevant digital tools and skills for their role.\n\nUnderstands and effectively applies appropriate methods, tools, applications and processes." + }, + { + "level": 4, + "description": "Maximises the capabilities of applications for their role and evaluates and supports the use of new technologies and digital tools.\n\nSelects appropriately from, and assesses the impact of change to applicable standards, methods, tools, applications and processes relevant to own specialism." + }, + { + "level": 5, + "description": "Recognises and evaluates the organisational impact of new technologies and digital services.\n\nImplements new and effective practices. \n\nAdvises on available standards, methods, tools, applications and processes relevant to group specialism(s) and can make appropriate choices from alternatives." + }, + { + "level": 6, + "description": "Leads the enhancement of the organisation’s digital capabilities. \n\nIdentifies and endorses opportunities to adopt new technologies and digital services.\n\nLeads digital governance and compliance with relevant legislation and the need for products and services." + }, + { + "level": 7, + "description": "Leads the development of the organisation’s digital culture and the transformational vision.  \n\nAdvances capability and/or exploitation of technology within one or more organisations through a deep understanding of the industry and the implications of emerging technologies.\n\nAccountable for assessing how laws and regulations impact organisational objectives and its use of digital, data and technology capabilities." + } + ] + }, + { + "code": "LEAD", + "name": "Leadership", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership", + "levels": [ + { + "level": 1, + "description": "Proactively increases their understanding of their work tasks and responsibilities." + }, + { + "level": 2, + "description": "Takes ownership to develop their work experience." + }, + { + "level": 3, + "description": "Provides basic guidance and support to less experienced team members as needed." + }, + { + "level": 4, + "description": "Leads, supports or guides team members.\n\nDevelops solutions for complex work activities related to assignments. \n\nDemonstrates an understanding of risk factors in their work.\n\nContributes specialist expertise to requirements definition in support of proposals." + }, + { + "level": 5, + "description": "Provides leadership at an operational level.\n\nImplements and executes policies aligned to strategic plans.\n\nAssesses and evaluates risk.\n\nTakes all requirements into account when considering proposals." + }, + { + "level": 6, + "description": "Provides leadership at an organisational level.\n\nContributes to the development and implementation of policy and strategy.\n\nUnderstands and communicates industry developments, and the role and impact of technology. \n\nManages and mitigates organisational risk.  \n\nBalances the requirements of proposals with the broader needs of the organisation." + }, + { + "level": 7, + "description": "Leads strategic management.\n\nApplies the highest level of leadership to the formulation and implementation of strategy.\n\nCommunicates the potential impact of emerging practices and technologies on organisations and individuals and assesses the risks of using or not using such practices and technologies. \n\nEstablishes governance to address business risk.\n\nEnsures proposals align with the strategic direction of the organisation." + } + ] + }, + { + "code": "LADV", + "name": "Learning and development", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning", + "levels": [ + { + "level": 1, + "description": "Applies newly acquired knowledge to develop  skills for their role. Contributes to identifying own development opportunities." + }, + { + "level": 2, + "description": "Absorbs and applies new information to tasks.\n\nRecognises personal skills and knowledge gaps and seeks learning opportunities to address them." + }, + { + "level": 3, + "description": "Absorbs and applies new information effectively with the ability to share learnings with colleagues.\n\nTakes the initiative in identifying and negotiating their own appropriate development opportunities." + }, + { + "level": 4, + "description": "Rapidly absorbs and critically assesses new information and applies it effectively.\n\nMaintains an understanding of emerging practices and their application and takes responsibility for driving own and team members’ development opportunities." + }, + { + "level": 5, + "description": "Uses their skills and knowledge to help establish the standards that others in the organisation will apply.\n\nTakes the initiative to develop a wider breadth of knowledge across industry and/or business and identify and manage development opportunities in area of responsibility." + }, + { + "level": 6, + "description": "Promotes the application of knowledge to support strategic imperatives.\n\nActively develops their strategic and technical leadership skills and leads the development of skills in their area of accountability." + }, + { + "level": 7, + "description": "Inspires a learning culture to align with business objectives.   \n\nMaintains strategic insight into contemporary and emerging industry landscapes. \n\nEnsures the organisation develops and mobilises the full range of required skills and capabilities." + } + ] + }, + { + "code": "PLAN", + "name": "Planning", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning", + "levels": [ + { + "level": 1, + "description": "Confirms required steps for individual tasks." + }, + { + "level": 2, + "description": "Plans own work within short time horizons in an organised way." + }, + { + "level": 3, + "description": "Organises and keeps track of own work (and others where needed) to meet agreed timescales." + }, + { + "level": 4, + "description": "Plans, schedules and monitors work to meet given personal and/or team objectives and processes, demonstrating an analytical approach to meet time and quality targets." + }, + { + "level": 5, + "description": "Analyses, designs, plans, establishes milestones, and executes and evaluates work to time, cost and quality targets." + }, + { + "level": 6, + "description": "Initiates and influences strategic objectives and assigns responsibilities." + }, + { + "level": 7, + "description": "Plans and leads at the highest level of authority over all aspects of a significant area of work." + } + ] + }, + { + "code": "PROB", + "name": "Problem-solving", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem", + "levels": [ + { + "level": 1, + "description": "Works towards understanding the issue and seeks assistance in resolving unexpected problems." + }, + { + "level": 2, + "description": "Investigates and resolves routine issues." + }, + { + "level": 3, + "description": "Applies a methodical approach to investigate and evaluate options to resolve routine and moderately complex issues." + }, + { + "level": 4, + "description": "Investigates the cause and impact, evaluates options and resolves a broad range of complex issues." + }, + { + "level": 5, + "description": "Investigates complex issues to identify the root causes and impacts, assesses a range of solutions, and makes informed decisions on the best course of action, often in collaboration with other experts." + }, + { + "level": 6, + "description": "Anticipates and leads in addressing problems and opportunities that may impact organisational objectives, establishing a strategic approach and allocating resources." + }, + { + "level": 7, + "description": "Manages inter-relationships between impacted parties and strategic imperatives, recognising the broader business context and drawing accurate conclusions when resolving problems." + } + ] + }, + { + "code": "ADAP", + "name": "Adaptability", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability", + "levels": [ + { + "level": 1, + "description": "Accepts change and is open to new ways of working." + }, + { + "level": 2, + "description": "Adjusts to different team dynamics and work requirements.\n\nParticipates in team adaptation processes." + }, + { + "level": 3, + "description": "Adapts and is responsive to change and shows initiative in adopting new methods or technologies." + }, + { + "level": 4, + "description": "Enables others to adapt and change in response to challenges and changes in the work environment." + }, + { + "level": 5, + "description": "Leads adaptations to changing business environments.\n\nGuides teams through transitions, maintaining focus on organisational objectives." + }, + { + "level": 6, + "description": "Drives organisational adaptability by initiating and leading significant changes. Influences change management strategies at an organisational level." + }, + { + "level": 7, + "description": "Champions organisational agility and resilience.\n\nEmbeds adaptability into organisational culture and strategic planning." + } + ] + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security", + "levels": [ + { + "level": 1, + "description": "Develops an understanding of the rules and expectations of their role and the organisation." + }, + { + "level": 2, + "description": "Has a good understanding of their role and the organisation’s rules and expectations." + }, + { + "level": 3, + "description": "Applies appropriate professionalism and working practices and knowledge to work." + }, + { + "level": 4, + "description": "Adapts and applies applicable standards, recognising their importance in achieving team outcomes." + }, + { + "level": 5, + "description": "Contributes proactively to the implementation of professional working practices and helps promote a supportive organisational culture." + }, + { + "level": 6, + "description": "Takes a leading role in promoting and ensuring appropriate culture and working practices, including the provision of equal access and opportunity to people with diverse abilities." + }, + { + "level": 7, + "description": "Provides clear direction and strategic leadership for embedding compliance, organisational culture, and working practices, and actively promotes diversity and inclusivity." + } + ] + } + ], + "by_level": [ + { + "level": 1, + "title": "Follow", + "factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Works mostly on their own tasks and interacts with their immediate team only. Develops an understanding of how their work supports others." + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates with immediate team to understand and deliver on their assigned tasks. Observes, listens, and with encouragement, asks questions to seek information or clarify instructions." + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Identifies opportunities for improvement in own tasks. Suggests basic enhancements when prompted." + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Participates in the generation of new ideas when prompted." + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses little discretion in attending to enquiries.  \n\nIs expected to seek guidance in unexpected situations." + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Has basic digital skills to learn and use applications, processes and tools for their role." + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Proactively increases their understanding of their work tasks and responsibilities." + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Applies newly acquired knowledge to develop  skills for their role. Contributes to identifying own development opportunities." + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Confirms required steps for individual tasks." + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Works towards understanding the issue and seeks assistance in resolving unexpected problems." + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Accepts change and is open to new ways of working." + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Develops an understanding of the rules and expectations of their role and the organisation." + } + ] + }, + { + "level": 2, + "title": "Assist", + "factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Understands the need to collaborate with their team and considers user/customer needs." + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates familiar information with immediate team and stakeholders directly related to their role.\n\nListens to gain understanding and asks relevant questions to clarify or seek further information." + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Proposes ideas to improve own work area.\n\nImplements agreed changes to assigned work tasks." + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Applies creative thinking to suggest new ways to approach a task and solve problems." + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses limited discretion in resolving issues or enquiries.\n\nDecides when to seek guidance in unexpected situations." + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Has sufficient digital skills for their role; understands and uses appropriate methods, tools, applications and processes." + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Takes ownership to develop their work experience." + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Absorbs and applies new information to tasks.\n\nRecognises personal skills and knowledge gaps and seeks learning opportunities to address them." + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Plans own work within short time horizons in an organised way." + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Investigates and resolves routine issues." + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Adjusts to different team dynamics and work requirements.\n\nParticipates in team adaptation processes." + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Has a good understanding of their role and the organisation’s rules and expectations." + } + ] + }, + { + "level": 3, + "title": "Apply", + "factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Understands and collaborates on the analysis of user/customer needs and represents this in their work." + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates with team and stakeholders inside and outside the organisation clearly explaining and presenting information.\n\nContributes to a range of work-related conversations and listens to others to gain an understanding and asks probing questions relevant to their role." + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Identifies and implements improvements in own work area.\n\nContributes to team-level process enhancements." + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Applies and contributes to creative thinking techniques to contribute new ideas for their own work and for team activities." + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses discretion in identifying and responding to complex issues related to own assignments. \n\nDetermines when issues should be escalated to a higher level." + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Explores and applies relevant digital tools and skills for their role.\n\nUnderstands and effectively applies appropriate methods, tools, applications and processes." + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Provides basic guidance and support to less experienced team members as needed." + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Absorbs and applies new information effectively with the ability to share learnings with colleagues.\n\nTakes the initiative in identifying and negotiating their own appropriate development opportunities." + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Organises and keeps track of own work (and others where needed) to meet agreed timescales." + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Applies a methodical approach to investigate and evaluate options to resolve routine and moderately complex issues." + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Adapts and is responsive to change and shows initiative in adopting new methods or technologies." + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Applies appropriate professionalism and working practices and knowledge to work." + } + ] + }, + { + "level": 4, + "title": "Enable", + "factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Facilitates collaboration between stakeholders who share common objectives.  \n\nEngages with and contributes to the work of cross-functional teams to ensure that user/customer needs are being met throughout the deliverable/scope of work." + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates with both technical and non-technical audiences including team and stakeholders inside and outside the organisation.\n\nAs required, takes the lead in explaining complex concepts to support decision making.\n\nListens and asks insightful questions to identify different perspectives to clarify and confirm understanding." + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Encourages and supports team discussions on improvement initiatives.\n\nImplements procedural changes within a defined scope of work." + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Applies, facilitates and develops creative thinking concepts and finds alternative ways to approach team outcomes." + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses judgment and substantial discretion in identifying and responding to complex issues and assignments related to projects and team objectives.\n\nEscalates when scope is impacted." + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Maximises the capabilities of applications for their role and evaluates and supports the use of new technologies and digital tools.\n\nSelects appropriately from, and assesses the impact of change to applicable standards, methods, tools, applications and processes relevant to own specialism." + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Leads, supports or guides team members.\n\nDevelops solutions for complex work activities related to assignments. \n\nDemonstrates an understanding of risk factors in their work.\n\nContributes specialist expertise to requirements definition in support of proposals." + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Rapidly absorbs and critically assesses new information and applies it effectively.\n\nMaintains an understanding of emerging practices and their application and takes responsibility for driving own and team members’ development opportunities." + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Plans, schedules and monitors work to meet given personal and/or team objectives and processes, demonstrating an analytical approach to meet time and quality targets." + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Investigates the cause and impact, evaluates options and resolves a broad range of complex issues." + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Enables others to adapt and change in response to challenges and changes in the work environment." + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Adapts and applies applicable standards, recognising their importance in achieving team outcomes." + } + ] + }, + { + "level": 5, + "title": "Ensure, advise", + "factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Facilitates collaboration between stakeholders who have diverse objectives.\n\nEnsures collaborative ways of working throughout all stages of work to meet user/customer needs.\n\nBuilds effective relationships across the organisation and with customers, suppliers and partners." + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates clearly with impact, articulating complex information and ideas to broad audiences with different viewpoints.\n\nLeads and encourages conversations to share ideas and build consensus on actions to be taken." + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Identifies and evaluates potential improvements to products, practices, or services.\n\nLeads implementation of enhancements within own area of responsibility.\n\nAssesses effectiveness of implemented changes." + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Creatively applies innovative thinking and design practices in identifying solutions that will deliver value for the benefit of the customer/stakeholder." + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses judgement to make informed decisions on actions to achieve organisational outcomes such as meeting targets, deadlines, and budget.\n\nRaises issues when objectives are at risk." + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Recognises and evaluates the organisational impact of new technologies and digital services.\n\nImplements new and effective practices. \n\nAdvises on available standards, methods, tools, applications and processes relevant to group specialism(s) and can make appropriate choices from alternatives." + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Provides leadership at an operational level.\n\nImplements and executes policies aligned to strategic plans.\n\nAssesses and evaluates risk.\n\nTakes all requirements into account when considering proposals." + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Uses their skills and knowledge to help establish the standards that others in the organisation will apply.\n\nTakes the initiative to develop a wider breadth of knowledge across industry and/or business and identify and manage development opportunities in area of responsibility." + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Analyses, designs, plans, establishes milestones, and executes and evaluates work to time, cost and quality targets." + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Investigates complex issues to identify the root causes and impacts, assesses a range of solutions, and makes informed decisions on the best course of action, often in collaboration with other experts." + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Leads adaptations to changing business environments.\n\nGuides teams through transitions, maintaining focus on organisational objectives." + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Contributes proactively to the implementation of professional working practices and helps promote a supportive organisational culture." + } + ] + }, + { + "level": 6, + "title": "Initiate, influence", + "factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Leads collaboration with a diverse range of stakeholders across competing objectives within the organisation.\n\nBuilds strong, influential connections with key internal and external contacts at senior management/technical leader level" + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates with credibility at all levels across the organisation to broad audiences with divergent objectives.\n\nExplains complex information and ideas clearly, influencing the strategic direction.\n\nPromotes information sharing across the organisation." + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Drives improvement initiatives that have a significant impact on the organisation.\n\nAligns improvement strategies with organisational objectives.\n\nEngages stakeholders in improvement processes." + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Creatively applies a wide range of new ideas and effective management techniques to achieve results that align with organisational strategy." + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses judgement to make decisions that initiate the achievement of agreed strategic objectives including financial performance.\n\nEscalates when broader strategic direction is impacted." + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Leads the enhancement of the organisation’s digital capabilities. \n\nIdentifies and endorses opportunities to adopt new technologies and digital services.\n\nLeads digital governance and compliance with relevant legislation and the need for products and services." + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Provides leadership at an organisational level.\n\nContributes to the development and implementation of policy and strategy.\n\nUnderstands and communicates industry developments, and the role and impact of technology. \n\nManages and mitigates organisational risk.  \n\nBalances the requirements of proposals with the broader needs of the organisation." + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Promotes the application of knowledge to support strategic imperatives.\n\nActively develops their strategic and technical leadership skills and leads the development of skills in their area of accountability." + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Initiates and influences strategic objectives and assigns responsibilities." + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Anticipates and leads in addressing problems and opportunities that may impact organisational objectives, establishing a strategic approach and allocating resources." + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Drives organisational adaptability by initiating and leading significant changes. Influences change management strategies at an organisational level." + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Takes a leading role in promoting and ensuring appropriate culture and working practices, including the provision of equal access and opportunity to people with diverse abilities." + } + ] + }, + { + "level": 7, + "title": "Set strategy, inspire, mobilise", + "factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Drives collaboration, engaging with leadership stakeholders ensuring alignment to corporate vision and strategy. \n\nBuilds strong, influential relationships with customers, partners and industry leaders." + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates to audiences at all levels within own organisation and engages with industry.\n\nPresents compelling arguments and ideas authoritatively and convincingly to achieve business objectives." + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Defines and communicates the organisational approach to continuous improvement.\n\nCultivates a culture of ongoing enhancement.\n\nEvaluates the impact of improvement initiatives on organisational success." + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Champions creativity and innovation in driving strategy development to enable business opportunities." + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses judgement in making decisions critical to the organisational strategic direction and success.\n\nEscalates when business executive management input is required through established governance structures." + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Leads the development of the organisation’s digital culture and the transformational vision.  \n\nAdvances capability and/or exploitation of technology within one or more organisations through a deep understanding of the industry and the implications of emerging technologies.\n\nAccountable for assessing how laws and regulations impact organisational objectives and its use of digital, data and technology capabilities." + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Leads strategic management.\n\nApplies the highest level of leadership to the formulation and implementation of strategy.\n\nCommunicates the potential impact of emerging practices and technologies on organisations and individuals and assesses the risks of using or not using such practices and technologies. \n\nEstablishes governance to address business risk.\n\nEnsures proposals align with the strategic direction of the organisation." + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Inspires a learning culture to align with business objectives.   \n\nMaintains strategic insight into contemporary and emerging industry landscapes. \n\nEnsures the organisation develops and mobilises the full range of required skills and capabilities." + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Plans and leads at the highest level of authority over all aspects of a significant area of work." + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Manages inter-relationships between impacted parties and strategic imperatives, recognising the broader business context and drawing accurate conclusions when resolving problems." + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Champions organisational agility and resilience.\n\nEmbeds adaptability into organisational culture and strategic planning." + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Provides clear direction and strategic leadership for embedding compliance, organisational culture, and working practices, and actively promotes diversity and inclusivity." + } + ] + } + ], + "source": { + "generic_attributes_url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours", + "behavioural_factors_pdf": "https://sfia-online.org/en/sfia-9/responsibilities/sfia-9-alternative-presentation-of-behavioural-factors.pdf" + } +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/en/index.json b/projects/odilo/data/sfia-9-json/en/index.json new file mode 100644 index 000000000..571980578 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/en/index.json @@ -0,0 +1,1052 @@ +{ + "type": "sfia.index", + "sfia_version": 9, + "language": "en", + "generated_at": "2026-02-02", + "source_file": "SFIA 9 Excel - English/sfia-9_current-standard_en_250129.xlsx", + "source_date": null, + "counts": { + "skills": 147, + "attributes": 16, + "levels_of_responsibility": 7 + }, + "paths": { + "skills_dir": "skills/", + "attributes": "attributes.json", + "levels_of_responsibility": "levels-of-responsibility.json", + "terms_of_use": "terms-of-use.json", + "responsibilities": "responsibilities.json", + "behaviour_matrix": "behaviour-matrix.json" + }, + "skills": [ + { + "code": "ITSP", + "name": "Strategic planning", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/ITSP.json" + }, + { + "code": "ISCO", + "name": "Information systems coordination", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/ISCO.json" + }, + { + "code": "IRMG", + "name": "Information management", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/IRMG.json" + }, + { + "code": "STPL", + "name": "Enterprise and business architecture", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/STPL.json" + }, + { + "code": "ARCH", + "name": "Solution architecture", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/ARCH.json" + }, + { + "code": "INOV", + "name": "Innovation management", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/INOV.json" + }, + { + "code": "EMRG", + "name": "Emerging technology monitoring", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/EMRG.json" + }, + { + "code": "RSCH", + "name": "Formal research", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/RSCH.json" + }, + { + "code": "SUST", + "name": "Sustainability", + "category": "Strategy and architecture", + "subcategory": "Strategy and planning", + "file": "skills/SUST.json" + }, + { + "code": "FMIT", + "name": "Financial management", + "category": "Strategy and architecture", + "subcategory": "Financial and value management", + "file": "skills/FMIT.json" + }, + { + "code": "INVA", + "name": "Investment appraisal", + "category": "Strategy and architecture", + "subcategory": "Financial and value management", + "file": "skills/INVA.json" + }, + { + "code": "BENM", + "name": "Benefits management", + "category": "Strategy and architecture", + "subcategory": "Financial and value management", + "file": "skills/BENM.json" + }, + { + "code": "BUDF", + "name": "Budgeting and forecasting", + "category": "Strategy and architecture", + "subcategory": "Financial and value management", + "file": "skills/BUDF.json" + }, + { + "code": "FIAN", + "name": "Financial analysis", + "category": "Strategy and architecture", + "subcategory": "Financial and value management", + "file": "skills/FIAN.json" + }, + { + "code": "COMG", + "name": "Cost management", + "category": "Strategy and architecture", + "subcategory": "Financial and value management", + "file": "skills/COMG.json" + }, + { + "code": "DEMM", + "name": "Demand management", + "category": "Strategy and architecture", + "subcategory": "Financial and value management", + "file": "skills/DEMM.json" + }, + { + "code": "MEAS", + "name": "Measurement", + "category": "Strategy and architecture", + "subcategory": "Financial and value management", + "file": "skills/MEAS.json" + }, + { + "code": "SCTY", + "name": "Information security", + "category": "Strategy and architecture", + "subcategory": "Security and privacy", + "file": "skills/SCTY.json" + }, + { + "code": "INAS", + "name": "Information assurance", + "category": "Strategy and architecture", + "subcategory": "Security and privacy", + "file": "skills/INAS.json" + }, + { + "code": "PEDP", + "name": "Information and data compliance", + "category": "Strategy and architecture", + "subcategory": "Security and privacy", + "file": "skills/PEDP.json" + }, + { + "code": "VURE", + "name": "Vulnerability research", + "category": "Strategy and architecture", + "subcategory": "Security and privacy", + "file": "skills/VURE.json" + }, + { + "code": "THIN", + "name": "Threat intelligence", + "category": "Strategy and architecture", + "subcategory": "Security and privacy", + "file": "skills/THIN.json" + }, + { + "code": "GOVN", + "name": "Governance", + "category": "Strategy and architecture", + "subcategory": "Governance, risk and compliance", + "file": "skills/GOVN.json" + }, + { + "code": "BURM", + "name": "Risk management", + "category": "Strategy and architecture", + "subcategory": "Governance, risk and compliance", + "file": "skills/BURM.json" + }, + { + "code": "AIDE", + "name": "Artificial intelligence (AI) and data ethics", + "category": "Strategy and architecture", + "subcategory": "Governance, risk and compliance", + "file": "skills/AIDE.json" + }, + { + "code": "AUDT", + "name": "Audit", + "category": "Strategy and architecture", + "subcategory": "Governance, risk and compliance", + "file": "skills/AUDT.json" + }, + { + "code": "QUMG", + "name": "Quality management", + "category": "Strategy and architecture", + "subcategory": "Governance, risk and compliance", + "file": "skills/QUMG.json" + }, + { + "code": "QUAS", + "name": "Quality assurance", + "category": "Strategy and architecture", + "subcategory": "Governance, risk and compliance", + "file": "skills/QUAS.json" + }, + { + "code": "CNSL", + "name": "Consultancy", + "category": "Strategy and architecture", + "subcategory": "Advice and guidance", + "file": "skills/CNSL.json" + }, + { + "code": "TECH", + "name": "Specialist advice", + "category": "Strategy and architecture", + "subcategory": "Advice and guidance", + "file": "skills/TECH.json" + }, + { + "code": "METL", + "name": "Methods and tools", + "category": "Strategy and architecture", + "subcategory": "Advice and guidance", + "file": "skills/METL.json" + }, + { + "code": "POMG", + "name": "Portfolio management", + "category": "Change and transformation", + "subcategory": "Change implementation", + "file": "skills/POMG.json" + }, + { + "code": "PGMG", + "name": "Programme management", + "category": "Change and transformation", + "subcategory": "Change implementation", + "file": "skills/PGMG.json" + }, + { + "code": "PRMG", + "name": "Project management", + "category": "Change and transformation", + "subcategory": "Change implementation", + "file": "skills/PRMG.json" + }, + { + "code": "PROF", + "name": "Portfolio, programme and project support", + "category": "Change and transformation", + "subcategory": "Change implementation", + "file": "skills/PROF.json" + }, + { + "code": "DEMG", + "name": "Delivery management", + "category": "Change and transformation", + "subcategory": "Change implementation", + "file": "skills/DEMG.json" + }, + { + "code": "BUSA", + "name": "Business situation analysis", + "category": "Change and transformation", + "subcategory": "Change analysis", + "file": "skills/BUSA.json" + }, + { + "code": "FEAS", + "name": "Feasibility assessment", + "category": "Change and transformation", + "subcategory": "Change analysis", + "file": "skills/FEAS.json" + }, + { + "code": "REQM", + "name": "Requirements definition and management", + "category": "Change and transformation", + "subcategory": "Change analysis", + "file": "skills/REQM.json" + }, + { + "code": "BSMO", + "name": "Business modelling", + "category": "Change and transformation", + "subcategory": "Change analysis", + "file": "skills/BSMO.json" + }, + { + "code": "BPTS", + "name": "User acceptance testing", + "category": "Change and transformation", + "subcategory": "Change analysis", + "file": "skills/BPTS.json" + }, + { + "code": "BPRE", + "name": "Business process improvement", + "category": "Change and transformation", + "subcategory": "Change planning", + "file": "skills/BPRE.json" + }, + { + "code": "OCDV", + "name": "Organisational capability development", + "category": "Change and transformation", + "subcategory": "Change planning", + "file": "skills/OCDV.json" + }, + { + "code": "JADN", + "name": "Job analysis and design", + "category": "Change and transformation", + "subcategory": "Change planning", + "file": "skills/JADN.json" + }, + { + "code": "ORDI", + "name": "Organisation design and implementation", + "category": "Change and transformation", + "subcategory": "Change planning", + "file": "skills/ORDI.json" + }, + { + "code": "CIPM", + "name": "Organisational change management", + "category": "Change and transformation", + "subcategory": "Change planning", + "file": "skills/CIPM.json" + }, + { + "code": "OCEN", + "name": "Organisational change enablement", + "category": "Change and transformation", + "subcategory": "Change planning", + "file": "skills/OCEN.json" + }, + { + "code": "PROD", + "name": "Product management", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/PROD.json" + }, + { + "code": "DLMG", + "name": "Systems development management", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/DLMG.json" + }, + { + "code": "SLEN", + "name": "Systems and software lifecycle engineering", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/SLEN.json" + }, + { + "code": "DESN", + "name": "Systems design", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/DESN.json" + }, + { + "code": "SWDN", + "name": "Software design", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/SWDN.json" + }, + { + "code": "NTDS", + "name": "Network design", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/NTDS.json" + }, + { + "code": "IFDN", + "name": "Infrastructure design", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/IFDN.json" + }, + { + "code": "HWDE", + "name": "Hardware design", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/HWDE.json" + }, + { + "code": "PROG", + "name": "Programming/software development", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/PROG.json" + }, + { + "code": "SINT", + "name": "Systems integration and build", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/SINT.json" + }, + { + "code": "TEST", + "name": "Functional testing", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/TEST.json" + }, + { + "code": "NFTS", + "name": "Non-functional testing", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/NFTS.json" + }, + { + "code": "PRTS", + "name": "Process testing", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/PRTS.json" + }, + { + "code": "PORT", + "name": "Software configuration", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/PORT.json" + }, + { + "code": "RESD", + "name": "Real-time/embedded systems development", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/RESD.json" + }, + { + "code": "SFEN", + "name": "Safety engineering", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/SFEN.json" + }, + { + "code": "SFAS", + "name": "Safety assessment", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/SFAS.json" + }, + { + "code": "RFEN", + "name": "Radio frequency engineering", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/RFEN.json" + }, + { + "code": "ADEV", + "name": "Animation development", + "category": "Development and implementation", + "subcategory": "Systems development", + "file": "skills/ADEV.json" + }, + { + "code": "DATM", + "name": "Data management", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/DATM.json" + }, + { + "code": "DTAN", + "name": "Data modelling and design", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/DTAN.json" + }, + { + "code": "DBDS", + "name": "Database design", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/DBDS.json" + }, + { + "code": "DAAN", + "name": "Data analytics", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/DAAN.json" + }, + { + "code": "DATS", + "name": "Data science", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/DATS.json" + }, + { + "code": "MLNG", + "name": "Machine learning", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/MLNG.json" + }, + { + "code": "BINT", + "name": "Business intelligence", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/BINT.json" + }, + { + "code": "DENG", + "name": "Data engineering", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/DENG.json" + }, + { + "code": "VISL", + "name": "Data visualisation", + "category": "Development and implementation", + "subcategory": "Data and analytics", + "file": "skills/VISL.json" + }, + { + "code": "URCH", + "name": "User research", + "category": "Development and implementation", + "subcategory": "User centred design", + "file": "skills/URCH.json" + }, + { + "code": "CEXP", + "name": "Customer experience", + "category": "Development and implementation", + "subcategory": "User centred design", + "file": "skills/CEXP.json" + }, + { + "code": "ACIN", + "name": "Accessibility and inclusion", + "category": "Development and implementation", + "subcategory": "User centred design", + "file": "skills/ACIN.json" + }, + { + "code": "UNAN", + "name": "User experience analysis", + "category": "Development and implementation", + "subcategory": "User centred design", + "file": "skills/UNAN.json" + }, + { + "code": "HCEV", + "name": "User experience design", + "category": "Development and implementation", + "subcategory": "User centred design", + "file": "skills/HCEV.json" + }, + { + "code": "USEV", + "name": "User experience evaluation", + "category": "Development and implementation", + "subcategory": "User centred design", + "file": "skills/USEV.json" + }, + { + "code": "INCA", + "name": "Content design and authoring", + "category": "Development and implementation", + "subcategory": "Content management", + "file": "skills/INCA.json" + }, + { + "code": "ICPM", + "name": "Content publishing", + "category": "Development and implementation", + "subcategory": "Content management", + "file": "skills/ICPM.json" + }, + { + "code": "KNOW", + "name": "Knowledge management", + "category": "Development and implementation", + "subcategory": "Content management", + "file": "skills/KNOW.json" + }, + { + "code": "GRDN", + "name": "Graphic design", + "category": "Development and implementation", + "subcategory": "Content management", + "file": "skills/GRDN.json" + }, + { + "code": "SCMO", + "name": "Scientific modelling", + "category": "Development and implementation", + "subcategory": "Computational science", + "file": "skills/SCMO.json" + }, + { + "code": "NUAN", + "name": "Numerical analysis", + "category": "Development and implementation", + "subcategory": "Computational science", + "file": "skills/NUAN.json" + }, + { + "code": "HPCC", + "name": "High-performance computing", + "category": "Development and implementation", + "subcategory": "Computational science", + "file": "skills/HPCC.json" + }, + { + "code": "ITMG", + "name": "Technology service management", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/ITMG.json" + }, + { + "code": "ASUP", + "name": "Application support", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/ASUP.json" + }, + { + "code": "ITOP", + "name": "Infrastructure operations", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/ITOP.json" + }, + { + "code": "SYSP", + "name": "System software administration", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/SYSP.json" + }, + { + "code": "NTAS", + "name": "Network support", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/NTAS.json" + }, + { + "code": "HSIN", + "name": "Systems installation and removal", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/HSIN.json" + }, + { + "code": "CFMG", + "name": "Configuration management", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/CFMG.json" + }, + { + "code": "RELM", + "name": "Release management", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/RELM.json" + }, + { + "code": "DEPL", + "name": "Deployment", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/DEPL.json" + }, + { + "code": "STMG", + "name": "Storage management", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/STMG.json" + }, + { + "code": "DCMA", + "name": "Facilities management", + "category": "Delivery and operation", + "subcategory": "Technology management", + "file": "skills/DCMA.json" + }, + { + "code": "SLMO", + "name": "Service level management", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/SLMO.json" + }, + { + "code": "SCMG", + "name": "Service catalogue management", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/SCMG.json" + }, + { + "code": "AVMT", + "name": "Availability management", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/AVMT.json" + }, + { + "code": "COPL", + "name": "Continuity management", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/COPL.json" + }, + { + "code": "CPMG", + "name": "Capacity management", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/CPMG.json" + }, + { + "code": "USUP", + "name": "Incident management", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/USUP.json" + }, + { + "code": "PBMG", + "name": "Problem management", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/PBMG.json" + }, + { + "code": "CHMG", + "name": "Change control", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/CHMG.json" + }, + { + "code": "ASMG", + "name": "Asset management", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/ASMG.json" + }, + { + "code": "SEAC", + "name": "Service acceptance", + "category": "Delivery and operation", + "subcategory": "Service management", + "file": "skills/SEAC.json" + }, + { + "code": "SCAD", + "name": "Security operations", + "category": "Delivery and operation", + "subcategory": "Security services", + "file": "skills/SCAD.json" + }, + { + "code": "IAMT", + "name": "Identity and access management", + "category": "Delivery and operation", + "subcategory": "Security services", + "file": "skills/IAMT.json" + }, + { + "code": "VUAS", + "name": "Vulnerability assessment", + "category": "Delivery and operation", + "subcategory": "Security services", + "file": "skills/VUAS.json" + }, + { + "code": "DGFS", + "name": "Digital forensics", + "category": "Delivery and operation", + "subcategory": "Security services", + "file": "skills/DGFS.json" + }, + { + "code": "CRIM", + "name": "Cybercrime investigation", + "category": "Delivery and operation", + "subcategory": "Security services", + "file": "skills/CRIM.json" + }, + { + "code": "OCOP", + "name": "Offensive cyber operations", + "category": "Delivery and operation", + "subcategory": "Security services", + "file": "skills/OCOP.json" + }, + { + "code": "PENT", + "name": "Penetration testing", + "category": "Delivery and operation", + "subcategory": "Security services", + "file": "skills/PENT.json" + }, + { + "code": "RMGT", + "name": "Records management", + "category": "Delivery and operation", + "subcategory": "Data and records operations", + "file": "skills/RMGT.json" + }, + { + "code": "ANCC", + "name": "Analytical classification and coding", + "category": "Delivery and operation", + "subcategory": "Data and records operations", + "file": "skills/ANCC.json" + }, + { + "code": "DBAD", + "name": "Database administration", + "category": "Delivery and operation", + "subcategory": "Data and records operations", + "file": "skills/DBAD.json" + }, + { + "code": "PEMT", + "name": "Performance management", + "category": "People and skills", + "subcategory": "People management", + "file": "skills/PEMT.json" + }, + { + "code": "EEXP", + "name": "Employee experience", + "category": "People and skills", + "subcategory": "People management", + "file": "skills/EEXP.json" + }, + { + "code": "OFCL", + "name": "Organisational facilitation", + "category": "People and skills", + "subcategory": "People management", + "file": "skills/OFCL.json" + }, + { + "code": "PDSV", + "name": "Professional development", + "category": "People and skills", + "subcategory": "People management", + "file": "skills/PDSV.json" + }, + { + "code": "WFPL", + "name": "Workforce planning", + "category": "People and skills", + "subcategory": "People management", + "file": "skills/WFPL.json" + }, + { + "code": "RESC", + "name": "Resourcing", + "category": "People and skills", + "subcategory": "People management", + "file": "skills/RESC.json" + }, + { + "code": "ETMG", + "name": "Learning and development management", + "category": "People and skills", + "subcategory": "Skills management", + "file": "skills/ETMG.json" + }, + { + "code": "TMCR", + "name": "Learning design and development", + "category": "People and skills", + "subcategory": "Skills management", + "file": "skills/TMCR.json" + }, + { + "code": "ETDL", + "name": "Learning delivery", + "category": "People and skills", + "subcategory": "Skills management", + "file": "skills/ETDL.json" + }, + { + "code": "LEDA", + "name": "Competency assessment", + "category": "People and skills", + "subcategory": "Skills management", + "file": "skills/LEDA.json" + }, + { + "code": "CSOP", + "name": "Certification scheme operation", + "category": "People and skills", + "subcategory": "Skills management", + "file": "skills/CSOP.json" + }, + { + "code": "TEAC", + "name": "Teaching", + "category": "People and skills", + "subcategory": "Skills management", + "file": "skills/TEAC.json" + }, + { + "code": "SUBF", + "name": "Subject formation", + "category": "People and skills", + "subcategory": "Skills management", + "file": "skills/SUBF.json" + }, + { + "code": "SORC", + "name": "Sourcing", + "category": "Relationships and engagement", + "subcategory": "Stakeholder management", + "file": "skills/SORC.json" + }, + { + "code": "SUPP", + "name": "Supplier management", + "category": "Relationships and engagement", + "subcategory": "Stakeholder management", + "file": "skills/SUPP.json" + }, + { + "code": "ITCM", + "name": "Contract management", + "category": "Relationships and engagement", + "subcategory": "Stakeholder management", + "file": "skills/ITCM.json" + }, + { + "code": "RLMT", + "name": "Stakeholder relationship management", + "category": "Relationships and engagement", + "subcategory": "Stakeholder management", + "file": "skills/RLMT.json" + }, + { + "code": "CSMG", + "name": "Customer service support", + "category": "Relationships and engagement", + "subcategory": "Stakeholder management", + "file": "skills/CSMG.json" + }, + { + "code": "ADMN", + "name": "Business administration", + "category": "Relationships and engagement", + "subcategory": "Stakeholder management", + "file": "skills/ADMN.json" + }, + { + "code": "BIDM", + "name": "Bid/proposal management", + "category": "Relationships and engagement", + "subcategory": "Sales and bid management", + "file": "skills/BIDM.json" + }, + { + "code": "SALE", + "name": "Selling", + "category": "Relationships and engagement", + "subcategory": "Sales and bid management", + "file": "skills/SALE.json" + }, + { + "code": "SSUP", + "name": "Sales support", + "category": "Relationships and engagement", + "subcategory": "Sales and bid management", + "file": "skills/SSUP.json" + }, + { + "code": "MKTG", + "name": "Marketing management", + "category": "Relationships and engagement", + "subcategory": "Marketing", + "file": "skills/MKTG.json" + }, + { + "code": "MRCH", + "name": "Market research", + "category": "Relationships and engagement", + "subcategory": "Marketing", + "file": "skills/MRCH.json" + }, + { + "code": "BRMG", + "name": "Brand management", + "category": "Relationships and engagement", + "subcategory": "Marketing", + "file": "skills/BRMG.json" + }, + { + "code": "CELO", + "name": "Customer engagement and loyalty", + "category": "Relationships and engagement", + "subcategory": "Marketing", + "file": "skills/CELO.json" + }, + { + "code": "MKCM", + "name": "Marketing campaign management", + "category": "Relationships and engagement", + "subcategory": "Marketing", + "file": "skills/MKCM.json" + }, + { + "code": "DIGM", + "name": "Digital marketing", + "category": "Relationships and engagement", + "subcategory": "Marketing", + "file": "skills/DIGM.json" + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/en/levels-of-responsibility.json b/projects/odilo/data/sfia-9-json/en/levels-of-responsibility.json new file mode 100644 index 000000000..6d730435b --- /dev/null +++ b/projects/odilo/data/sfia-9-json/en/levels-of-responsibility.json @@ -0,0 +1,49 @@ +{ + "type": "sfia.levels_of_responsibility", + "sfia_version": 9, + "language": "en", + "items": [ + { + "level": 1, + "guiding_phrase": "Follow", + "essence": "Essence of the level: Performs routine tasks under close supervision, follows instructions, and requires guidance to complete their work. Learns and applies basic skills and knowledge.", + "url": "https://sfia-online.org/en/lor/9/1" + }, + { + "level": 2, + "guiding_phrase": "Assist", + "essence": "Essence of the level: Provides assistance to others, works under routine supervision, and uses their discretion to address routine problems. Actively learns through training and on-the-job experiences.", + "url": "https://sfia-online.org/en/lor/9/2" + }, + { + "level": 3, + "guiding_phrase": "Apply", + "essence": "Essence of the level: Performs varied tasks, sometimes complex and non-routine, using standard methods and procedures. Works under general direction, exercises discretion, and manages own work within deadlines. Proactively enhances skills and impact in the workplace.", + "url": "https://sfia-online.org/en/lor/9/3" + }, + { + "level": 4, + "guiding_phrase": "Enable", + "essence": "Essence of the level: Performs diverse complex activities, supports and guides others, delegates tasks when appropriate, works autonomously under general direction, and contributes expertise to deliver team objectives.", + "url": "https://sfia-online.org/en/lor/9/4" + }, + { + "level": 5, + "guiding_phrase": "Ensure, advise", + "essence": "Essence of the level: Provides authoritative guidance in their field and works under broad direction. Accountable for delivering significant work outcomes, from analysis through execution to evaluation.", + "url": "https://sfia-online.org/en/lor/9/5" + }, + { + "level": 6, + "guiding_phrase": "Initiate, influence", + "essence": "Essence of the level: Has significant organisational influence, makes high-level decisions, shapes policies, demonstrates leadership, promotes organisational collaboration, and accepts accountability in key areas.", + "url": "https://sfia-online.org/en/lor/9/6" + }, + { + "level": 7, + "guiding_phrase": "Set strategy, inspire, mobilise", + "essence": "Essence of the level: Operates at the highest organisational level, determines overall organisational vision and strategy, and assumes accountability for overall success.", + "url": "https://sfia-online.org/en/lor/9/7" + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/en/responsibilities.json b/projects/odilo/data/sfia-9-json/en/responsibilities.json new file mode 100644 index 000000000..c7376ef51 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/en/responsibilities.json @@ -0,0 +1,762 @@ +{ + "type": "sfia.responsibilities", + "sfia_version": 9, + "language": "en", + "guidance_notes": "SFIA Levels represent levels of responsibility in the workplace. Each successive level describes increasing impact, responsibility and accountability.\n• Autonomy, influence and complexity are generic attributes that indicate the level of responsibility.\n• Business skills and behavioural factors describe the behaviours required to be effective at each level.\n• The knowledge attribute defines the depth and breadth of understanding required to perform and influence work effectively.\nUnderstanding these attributes will help you get the most out of SFIA. They are critical to understanding and applying the levels described in the SFIA skill descriptions.", + "levels": [ + { + "level": 1, + "title": "Follow", + "guiding_phrase": "Follow", + "essence": "Essence of the level: Performs routine tasks under close supervision, follows instructions, and requires guidance to complete their work. Learns and applies basic skills and knowledge.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-1", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomy", + "description": "Follows instructions and works under close direction. Receives specific instructions and guidance, has work closely reviewed.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influence", + "description": "When required, contributes to team discussions with immediate colleagues.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexity", + "description": "Performs routine activities in a structured environment.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Knowledge", + "description": "Applies basic knowledge to perform routine, well-defined, predictable role-specific tasks.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Works mostly on their own tasks and interacts with their immediate team only. Develops an understanding of how their work supports others.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates with immediate team to understand and deliver on their assigned tasks. Observes, listens, and with encouragement, asks questions to seek information or clarify instructions.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Identifies opportunities for improvement in own tasks. Suggests basic enhancements when prompted.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Participates in the generation of new ideas when prompted.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses little discretion in attending to enquiries.  \n\nIs expected to seek guidance in unexpected situations.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Has basic digital skills to learn and use applications, processes and tools for their role.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Proactively increases their understanding of their work tasks and responsibilities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Applies newly acquired knowledge to develop  skills for their role. Contributes to identifying own development opportunities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Confirms required steps for individual tasks.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Works towards understanding the issue and seeks assistance in resolving unexpected problems.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Accepts change and is open to new ways of working.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Develops an understanding of the rules and expectations of their role and the organisation.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 2, + "title": "Assist", + "guiding_phrase": "Assist", + "essence": "Essence of the level: Provides assistance to others, works under routine supervision, and uses their discretion to address routine problems. Actively learns through training and on-the-job experiences.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-2", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomy", + "description": "Works under routine direction. Receives instructions and guidance, has work regularly reviewed.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influence", + "description": "Is expected to contribute to team discussions with immediate team members. Works alongside team members, contributing to team decisions. When the role requires, interacts with people outside their team, including internal colleagues and external contacts.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexity", + "description": "Performs a range of work activities in varied environments.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Knowledge", + "description": "Applies knowledge of common workplace tasks and practices to support team activities under guidance.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Understands the need to collaborate with their team and considers user/customer needs.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates familiar information with immediate team and stakeholders directly related to their role.\n\nListens to gain understanding and asks relevant questions to clarify or seek further information.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Proposes ideas to improve own work area.\n\nImplements agreed changes to assigned work tasks.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Applies creative thinking to suggest new ways to approach a task and solve problems.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses limited discretion in resolving issues or enquiries.\n\nDecides when to seek guidance in unexpected situations.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Has sufficient digital skills for their role; understands and uses appropriate methods, tools, applications and processes.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Takes ownership to develop their work experience.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Absorbs and applies new information to tasks.\n\nRecognises personal skills and knowledge gaps and seeks learning opportunities to address them.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Plans own work within short time horizons in an organised way.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Investigates and resolves routine issues.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Adjusts to different team dynamics and work requirements.\n\nParticipates in team adaptation processes.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Has a good understanding of their role and the organisation’s rules and expectations.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 3, + "title": "Apply", + "guiding_phrase": "Apply", + "essence": "Essence of the level: Performs varied tasks, sometimes complex and non-routine, using standard methods and procedures. Works under general direction, exercises discretion, and manages own work within deadlines. Proactively enhances skills and impact in the workplace.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-3", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomy", + "description": "Works under general direction to complete assigned tasks. Receives guidance and has work reviewed at agreed milestones. When required, delegates routine tasks to others within own team.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influence", + "description": "Works with and influences team decisions. Has a transactional level of contact with people outside their team, including internal colleagues and external contacts.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexity", + "description": "Performs a range of work, sometimes complex and non-routine, in varied environments.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Knowledge", + "description": "Applies knowledge of a range of role-specific practices to complete tasks within defined boundaries and has an appreciation of how this knowledge applies to the wider business context.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Understands and collaborates on the analysis of user/customer needs and represents this in their work.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates with team and stakeholders inside and outside the organisation clearly explaining and presenting information.\n\nContributes to a range of work-related conversations and listens to others to gain an understanding and asks probing questions relevant to their role.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Identifies and implements improvements in own work area.\n\nContributes to team-level process enhancements.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Applies and contributes to creative thinking techniques to contribute new ideas for their own work and for team activities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses discretion in identifying and responding to complex issues related to own assignments. \n\nDetermines when issues should be escalated to a higher level.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Explores and applies relevant digital tools and skills for their role.\n\nUnderstands and effectively applies appropriate methods, tools, applications and processes.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Provides basic guidance and support to less experienced team members as needed.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Absorbs and applies new information effectively with the ability to share learnings with colleagues.\n\nTakes the initiative in identifying and negotiating their own appropriate development opportunities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Organises and keeps track of own work (and others where needed) to meet agreed timescales.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Applies a methodical approach to investigate and evaluate options to resolve routine and moderately complex issues.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Adapts and is responsive to change and shows initiative in adopting new methods or technologies.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Applies appropriate professionalism and working practices and knowledge to work.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 4, + "title": "Enable", + "guiding_phrase": "Enable", + "essence": "Essence of the level: Performs diverse complex activities, supports and guides others, delegates tasks when appropriate, works autonomously under general direction, and contributes expertise to deliver team objectives.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-4", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomy", + "description": "Works under general direction within a clear framework of accountability. Exercises considerable personal responsibility and autonomy. When required, plans, schedules, and delegates work to others, typically within own team.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influence", + "description": "Influences projects and team objectives. Has a tactical level of contact with people outside their team, including internal colleagues and external contacts.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexity", + "description": "Work includes a broad range of complex technical or professional activities in varied contexts.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Knowledge", + "description": "Applies knowledge across different areas in their field, integrating this knowledge to perform complex and diverse tasks. Applies a working knowledge of the organisation’s domain.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Facilitates collaboration between stakeholders who share common objectives.  \n\nEngages with and contributes to the work of cross-functional teams to ensure that user/customer needs are being met throughout the deliverable/scope of work.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates with both technical and non-technical audiences including team and stakeholders inside and outside the organisation.\n\nAs required, takes the lead in explaining complex concepts to support decision making.\n\nListens and asks insightful questions to identify different perspectives to clarify and confirm understanding.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Encourages and supports team discussions on improvement initiatives.\n\nImplements procedural changes within a defined scope of work.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Applies, facilitates and develops creative thinking concepts and finds alternative ways to approach team outcomes.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses judgment and substantial discretion in identifying and responding to complex issues and assignments related to projects and team objectives.\n\nEscalates when scope is impacted.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Maximises the capabilities of applications for their role and evaluates and supports the use of new technologies and digital tools.\n\nSelects appropriately from, and assesses the impact of change to applicable standards, methods, tools, applications and processes relevant to own specialism.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Leads, supports or guides team members.\n\nDevelops solutions for complex work activities related to assignments. \n\nDemonstrates an understanding of risk factors in their work.\n\nContributes specialist expertise to requirements definition in support of proposals.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Rapidly absorbs and critically assesses new information and applies it effectively.\n\nMaintains an understanding of emerging practices and their application and takes responsibility for driving own and team members’ development opportunities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Plans, schedules and monitors work to meet given personal and/or team objectives and processes, demonstrating an analytical approach to meet time and quality targets.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Investigates the cause and impact, evaluates options and resolves a broad range of complex issues.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Enables others to adapt and change in response to challenges and changes in the work environment.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Adapts and applies applicable standards, recognising their importance in achieving team outcomes.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 5, + "title": "Ensure, advise", + "guiding_phrase": "Ensure, advise", + "essence": "Essence of the level: Provides authoritative guidance in their field and works under broad direction. Accountable for delivering significant work outcomes, from analysis through execution to evaluation.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-5", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomy", + "description": "Works under broad direction. Work is self-initiated, consistent with agreed operational and budgetary requirements for meeting allocated technical and/or group objectives. Defines tasks and delegates work to teams and individuals within area of responsibility.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influence", + "description": "Influences critical decisions in their domain.  Has operational level contact impacting execution and implementation with internal colleagues and external contacts. Has significant influence over the allocation and management of resources required to deliver projects.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexity", + "description": "Performs an extensive range of complex technical and/or professional work activities, requiring the application of fundamental principles in a range of unpredictable contexts.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Knowledge", + "description": "Applies knowledge to interpret complex situations and offer authoritative advice. Applies in-depth expertise in specific fields, with a broader understanding across industry/business.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Facilitates collaboration between stakeholders who have diverse objectives.\n\nEnsures collaborative ways of working throughout all stages of work to meet user/customer needs.\n\nBuilds effective relationships across the organisation and with customers, suppliers and partners.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates clearly with impact, articulating complex information and ideas to broad audiences with different viewpoints.\n\nLeads and encourages conversations to share ideas and build consensus on actions to be taken.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Identifies and evaluates potential improvements to products, practices, or services.\n\nLeads implementation of enhancements within own area of responsibility.\n\nAssesses effectiveness of implemented changes.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Creatively applies innovative thinking and design practices in identifying solutions that will deliver value for the benefit of the customer/stakeholder.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses judgement to make informed decisions on actions to achieve organisational outcomes such as meeting targets, deadlines, and budget.\n\nRaises issues when objectives are at risk.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Recognises and evaluates the organisational impact of new technologies and digital services.\n\nImplements new and effective practices. \n\nAdvises on available standards, methods, tools, applications and processes relevant to group specialism(s) and can make appropriate choices from alternatives.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Provides leadership at an operational level.\n\nImplements and executes policies aligned to strategic plans.\n\nAssesses and evaluates risk.\n\nTakes all requirements into account when considering proposals.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Uses their skills and knowledge to help establish the standards that others in the organisation will apply.\n\nTakes the initiative to develop a wider breadth of knowledge across industry and/or business and identify and manage development opportunities in area of responsibility.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Analyses, designs, plans, establishes milestones, and executes and evaluates work to time, cost and quality targets.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Investigates complex issues to identify the root causes and impacts, assesses a range of solutions, and makes informed decisions on the best course of action, often in collaboration with other experts.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Leads adaptations to changing business environments.\n\nGuides teams through transitions, maintaining focus on organisational objectives.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Contributes proactively to the implementation of professional working practices and helps promote a supportive organisational culture.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 6, + "title": "Initiate, influence", + "guiding_phrase": "Initiate, influence", + "essence": "Essence of the level: Has significant organisational influence, makes high-level decisions, shapes policies, demonstrates leadership, promotes organisational collaboration, and accepts accountability in key areas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-6", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomy", + "description": "Guides high level decisions and strategies within the organisation’s overall policies and objectives. Has defined authority and accountability for actions and decisions within a significant area of work, including technical, financial and quality aspects. Delegates responsibility for operational objectives.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influence", + "description": "Influences the formation of strategy and the execution of business plans. Has a significant management level of contact with internal colleagues and external contacts. Has organisational leadership and influence over the appointment and management of resources related to the implementation of strategic initiatives.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexity", + "description": "Performs highly complex work activities covering technical, financial and quality aspects.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Knowledge", + "description": "Applies broad business knowledge to enable strategic leadership and decision-making across various domains.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Leads collaboration with a diverse range of stakeholders across competing objectives within the organisation.\n\nBuilds strong, influential connections with key internal and external contacts at senior management/technical leader level", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates with credibility at all levels across the organisation to broad audiences with divergent objectives.\n\nExplains complex information and ideas clearly, influencing the strategic direction.\n\nPromotes information sharing across the organisation.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Drives improvement initiatives that have a significant impact on the organisation.\n\nAligns improvement strategies with organisational objectives.\n\nEngages stakeholders in improvement processes.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Creatively applies a wide range of new ideas and effective management techniques to achieve results that align with organisational strategy.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses judgement to make decisions that initiate the achievement of agreed strategic objectives including financial performance.\n\nEscalates when broader strategic direction is impacted.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Leads the enhancement of the organisation’s digital capabilities. \n\nIdentifies and endorses opportunities to adopt new technologies and digital services.\n\nLeads digital governance and compliance with relevant legislation and the need for products and services.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Provides leadership at an organisational level.\n\nContributes to the development and implementation of policy and strategy.\n\nUnderstands and communicates industry developments, and the role and impact of technology. \n\nManages and mitigates organisational risk.  \n\nBalances the requirements of proposals with the broader needs of the organisation.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Promotes the application of knowledge to support strategic imperatives.\n\nActively develops their strategic and technical leadership skills and leads the development of skills in their area of accountability.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Initiates and influences strategic objectives and assigns responsibilities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Anticipates and leads in addressing problems and opportunities that may impact organisational objectives, establishing a strategic approach and allocating resources.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Drives organisational adaptability by initiating and leading significant changes. Influences change management strategies at an organisational level.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Takes a leading role in promoting and ensuring appropriate culture and working practices, including the provision of equal access and opportunity to people with diverse abilities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 7, + "title": "Set strategy, inspire, mobilise", + "guiding_phrase": "Set strategy, inspire, mobilise", + "essence": "Essence of the level: Operates at the highest organisational level, determines overall organisational vision and strategy, and assumes accountability for overall success.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-7", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomy", + "description": "Defines and leads the organisation’s vision and strategy within over-arching business objectives. Is fully accountable for actions taken and decisions made, both by self and others to whom responsibilities have been assigned. Delegates authority and responsibility for strategic business objectives.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influence", + "description": "Directs, influences and inspires the strategic direction and development of the organisation. Has an extensive leadership level of contact with internal colleagues and external contacts. Authorises the appointment of required resources.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexity", + "description": "Performs extensive strategic leadership in delivering business value through vision, governance and executive management.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Knowledge", + "description": "Applies strategic and broad-based knowledge to shape organisational strategy, anticipate future industry trends, and prepare the organisation to adapt and lead.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Collaboration", + "description": "Drives collaboration, engaging with leadership stakeholders ensuring alignment to corporate vision and strategy. \n\nBuilds strong, influential relationships with customers, partners and industry leaders.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Communication", + "description": "Communicates to audiences at all levels within own organisation and engages with industry.\n\nPresents compelling arguments and ideas authoritatively and convincingly to achieve business objectives.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Improvement mindset", + "description": "Defines and communicates the organisational approach to continuous improvement.\n\nCultivates a culture of ongoing enhancement.\n\nEvaluates the impact of improvement initiatives on organisational success.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creativity", + "description": "Champions creativity and innovation in driving strategy development to enable business opportunities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Decision-making", + "description": "Uses judgement in making decisions critical to the organisational strategic direction and success.\n\nEscalates when business executive management input is required through established governance structures.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Digital mindset", + "description": "Leads the development of the organisation’s digital culture and the transformational vision.  \n\nAdvances capability and/or exploitation of technology within one or more organisations through a deep understanding of the industry and the implications of emerging technologies.\n\nAccountable for assessing how laws and regulations impact organisational objectives and its use of digital, data and technology capabilities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Leadership", + "description": "Leads strategic management.\n\nApplies the highest level of leadership to the formulation and implementation of strategy.\n\nCommunicates the potential impact of emerging practices and technologies on organisations and individuals and assesses the risks of using or not using such practices and technologies. \n\nEstablishes governance to address business risk.\n\nEnsures proposals align with the strategic direction of the organisation.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Learning and development", + "description": "Inspires a learning culture to align with business objectives.   \n\nMaintains strategic insight into contemporary and emerging industry landscapes. \n\nEnsures the organisation develops and mobilises the full range of required skills and capabilities.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planning", + "description": "Plans and leads at the highest level of authority over all aspects of a significant area of work.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Problem-solving", + "description": "Manages inter-relationships between impacted parties and strategic imperatives, recognising the broader business context and drawing accurate conclusions when resolving problems.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptability", + "description": "Champions organisational agility and resilience.\n\nEmbeds adaptability into organisational culture and strategic planning.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Security, privacy and ethics", + "description": "Provides clear direction and strategic leadership for embedding compliance, organisational culture, and working practices, and actively promotes diversity and inclusivity.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + } + ], + "source": { + "base_url": "https://sfia-online.org/en/sfia-9/responsibilities", + "generic_attributes_url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours", + "behavioural_factors_pdf": "https://sfia-online.org/en/sfia-9/responsibilities/sfia-9-alternative-presentation-of-behavioural-factors.pdf" + } +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/en/terms-of-use.json b/projects/odilo/data/sfia-9-json/en/terms-of-use.json new file mode 100644 index 000000000..40481eb4d --- /dev/null +++ b/projects/odilo/data/sfia-9-json/en/terms-of-use.json @@ -0,0 +1,7 @@ +{ + "type": "sfia.terms_of_use", + "sfia_version": 9, + "language": "en", + "text": "Intellectual property and copyright\n\nThe Intellectual Property Rights subsisting in the Skills Framework for the Information Age (“SFIA”), in the SFIA name and in any documentation, information, designs and logos issued by the SFIA Foundation shall be and shall remain the sole property of The Foundation. The Foundation holds copyright of all the above mentioned items.\n\nReproduction or distribution of SFIA in any form or medium is prohibited unless specifically permitted by licence.\n\nIf you have a Corporate user SFIA license, you are permitted to share this Excel file with colleagues within the same organisation.\n\nHowever, please do not distribute it beyond your organisation. We would like users outside your organisation to obtain their own SFIA license and download the Excel file directly from the official source.\n\nTranslations\n\nTranslation into any language, language variant or dialect without specific permission is prohibited. Any party wishing to do this should approach the Foundation to discuss a special arrangement. In the case of such a translation, the Foundation shall own the intellectual property and the copyright subsisting in the translated work.", + "note": null +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/es/attributes.json b/projects/odilo/data/sfia-9-json/es/attributes.json new file mode 100644 index 000000000..7570e854f --- /dev/null +++ b/projects/odilo/data/sfia-9-json/es/attributes.json @@ -0,0 +1,743 @@ +{ + "type": "sfia.attributes", + "sfia_version": 9, + "language": "es", + "items": [ + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "AUTO", + "name": "Autonomía", + "url": "https://sfia-online.org/es/shortcode/9/AUTO", + "attribute_type": "Attributes", + "overall_description": "El nivel de independencia, discreción y responsabilidad por los resultados en su función.", + "guidance_notes": "En SFIA, la autonomía representa una progresión desde seguir instrucciones hasta definir una estrategia organizativa. Implica:\n\ntrabajar bajo distintos niveles de dirección y supervisión\ntomar decisiones independientes en consonancia con la responsabilidad\nasumir la responsabilidad por las acciones y sus resultados\ndelegar adecuadamente las tareas y responsabilidades\nestablecer metas individuales, grupales y organizacionales.\n\nLa autonomía efectiva abarca las habilidades de toma de decisiones, la autogestión y la capacidad de equilibrar la independencia con los objetivos de la organización. La autonomía está estrechamente vinculada con habilidades como la toma de decisiones, el liderazgo y la planificación.\nA medida que los profesionales avanzan, su nivel de autonomía moldea cada vez más su capacidad para impulsar el cambio, innovar y contribuir al éxito organizacional. A medida que los profesionales avanzan, su autonomía les permite liderar iniciativas e impulsar resultados estratégicos. A niveles más altos, las personas moldean su papel y toman decisiones que tienen un impacto organizacional más amplio, con una supervisión mínima.", + "levels": [ + { + "level": 1, + "description": "Sigue instrucciones y trabaja bajo una dirección cercana. Recibe instrucciones y orientaciones específicas, hace revisar de cerca el trabajo." + }, + { + "level": 2, + "description": "Trabaja bajo dirección rutinaria. Recibe instrucciones y orientación, su trabajo es revisado regularmente." + }, + { + "level": 3, + "description": "Trabaja bajo dirección general para completar las tareas asignadas. Recibe orientación y hace revisar el trabajo en hitos acordados. Cuando es necesario, delega tareas rutinarias a otros dentro de su propio equipo." + }, + { + "level": 4, + "description": "Trabaja bajo dirección general dentro de un claro marco de rendición de cuentas. Ejerce una considerable responsabilidad personal y autonomía. Cuando es necesario, planifica, programa y delega el trabajo en otros, normalmente dentro del propio equipo." + }, + { + "level": 5, + "description": "Trabaja bajo una dirección amplia. El trabajo es de iniciativa propia, coherente con los requisitos operativos y presupuestarios acordados para cumplir los objetivos técnicos y/o grupales asignados. Define tareas y delega el trabajo en equipos e individuos dentro de su área de responsabilidad." + }, + { + "level": 6, + "description": "Orienta las decisiones y estrategias de alto nivel dentro de las políticas y objetivos generales de la organización. Tiene autoridad y rendición de cuentas definidas por las acciones y decisiones dentro de un área significativa de trabajo, incluso aspectos técnicos, financieros y de calidad. Delega la responsabilidad de los objetivos operativos." + }, + { + "level": 7, + "description": "Define y lidera la visión y estrategia de la organización dentro de los objetivos de negocio amplios. Es totalmente responsable de las acciones realizadas y las decisiones tomadas, tanto por sí mismo como por otros a quienes se les han asignado responsabilidades. Delega autoridad y responsabilidad en los objetivos estratégicos del negocio." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "INFL", + "name": "Influencia", + "url": "https://sfia-online.org/es/shortcode/9/INFL", + "attribute_type": "Attributes", + "overall_description": "El alcance y el impacto de sus decisiones y acciones, tanto dentro como fuera de la organización.", + "guidance_notes": "La influencia en SFIA refleja una progresión que va desde el impacto directo en los colegas hasta la configuración de la dirección organizacional. Implica:\n\nampliar la esfera de interacción e impacto\nprogresar de las interacciones transaccionales a las estratégicas\ncolaborar con las partes interesadas, tanto internos como externos, en niveles cada vez mayores de antigüedad\nconfigurar decisiones con un impacto organizativo creciente\ncontribuir a la dirección grupal, departamental y organizacional.\n\nLa influencia está estrechamente vinculada con otros atributos como, la comunicación y el liderazgo. La influencia efectiva se desarrolla a través de la experiencia y la interacción con niveles más altos de la organización y la industria. Este atributo refleja el alcance y el impacto de las decisiones y acciones, tanto dentro como fuera de la organización. \nA medida que los profesionales avanzan, su influencia se extiende más allá de su equipo, contribuyendo a las decisiones estratégicas y ayudando a dar forma a la dirección de la organización. Progresa desde la conciencia de cómo el trabajo propio apoya a otros, hasta la dirección de la estrategia a nivel organizacional. El grado de influencia a menudo se refleja en la naturaleza de las interacciones, el nivel de contactos y el impacto de las decisiones en la dirección organizacional.", + "levels": [ + { + "level": 1, + "description": "Cuando es necesario, contribuye a las discusiones del equipo con sus colegas inmediatos." + }, + { + "level": 2, + "description": "Se espera que contribuya a las discusiones del equipo con los miembros inmediatos del equipo. Trabaja junto a los miembros del equipo, contribuyendo a las decisiones del equipo. Cuando el rol lo requiere, interactúa con personas fuera de su equipo, incluyendo colegas internos y contactos externos." + }, + { + "level": 3, + "description": "Trabaja e influye en las decisiones del equipo. Tiene un nivel transaccional de contacto con personas ajenas a su equipo, incluso colegas internos y contactos externos." + }, + { + "level": 4, + "description": "Influye en los proyectos y en los objetivos del equipo. Tiene un nivel táctico de contacto con personas ajenas a su equipo, incluidos colegas internos y contactos externos." + }, + { + "level": 5, + "description": "Influye en las decisiones críticas bajo su dominio. Mantiene contacto a nivel operativo que impacta en la ejecución e implementación con colegas internos y contactos externos. Tiene influencia considerable en la asignación y gestión de los recursos requeridos para entregar los proyectos." + }, + { + "level": 6, + "description": "Influye en la formación de la estrategia y en la ejecución de los planes de negocio. Tiene un nivel gerencial significativo de contacto con colegas internos y contactos externos. Tiene liderazgo organizacional e influencia sobre el nombramiento y manejo de recursos relacionados con la implementación de iniciativas estratégicas." + }, + { + "level": 7, + "description": "Dirige, influye e inspira la dirección estratégica y el desarrollo de la organización. Tiene un amplio nivel de liderazgo de contacto con colegas internos y contactos externos. Autoriza el nombramiento de los recursos requeridos." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "COMP", + "name": "Complejidad", + "url": "https://sfia-online.org/es/shortcode/9/COMP", + "attribute_type": "Attributes", + "overall_description": "La variedad y complejidad de las tareas y responsabilidades que conlleva su función.", + "guidance_notes": "En SFIA, la complejidad representa una progresión de las tareas rutinarias al liderazgo estratégico que ofrece valor al negocio. Implica:\n\nmanejar entornos de trabajo cada vez más variados e impredecibles\nabordar una gama creciente de actividades técnicas o profesionales\nresolver problemas progresivamente complejos\ngestionar diversas partes interesadas\ncontribuir a la política y la estrategia\naprovechar las tecnologías emergentes para obtener valor empresarial.\n\nLa gestión eficaz de la complejidad abarca las habilidades para la solución de problemas, la adopción de decisiones y la planificación, junto con los conocimientos técnicos o profesionales. Este atributo refleja el alcance y la complejidad de las tareas y responsabilidades de una función, pasando desde las actividades rutinarias hasta un amplio liderazgo estratégico. Puede medirse por el nivel de solución de problemas requerido, la naturaleza y el número de partes interesadas participantes, y el impacto de las decisiones adoptadas.\nA medida que los profesionales avanzan, su capacidad para navegar y aprovechar la complejidad contribuye cada vez más a la innovación organizativa, la eficiencia y la ventaja competitiva.", + "levels": [ + { + "level": 1, + "description": "Realiza actividades rutinarias en un ambiente estructurado." + }, + { + "level": 2, + "description": "Realiza una variedad de actividades laborales en diversos entornos." + }, + { + "level": 3, + "description": "Realiza una variedad de trabajos, a veces complejos y no rutinarios, en ambientes variados." + }, + { + "level": 4, + "description": "El trabajo incluye una amplia gama de actividades técnicas o profesionales complejas en diversos contextos." + }, + { + "level": 5, + "description": "Realiza una amplia gama de actividades complejas de trabajo técnico y/o profesional, que requieren la aplicación de principios fundamentales en una gama de contextos impredecibles." + }, + { + "level": 6, + "description": "Realiza actividades laborales de alta complejidad que abarcan aspectos técnicos, financieros y de calidad." + }, + { + "level": 7, + "description": "Ejerce un amplio liderazgo estratégico en la entrega de valor empresarial a través de visión, gobernanza y gestión ejecutiva." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "KNGE", + "name": "Conocimiento", + "url": "https://sfia-online.org/es/shortcode/9/KNGE", + "attribute_type": "Attributes", + "overall_description": "La profundidad y amplitud de la comprensión requerida para realizar e influir en el trabajo de manera efectiva.", + "guidance_notes": "En SFIA, el conocimiento representa una progresión desde la aplicación de información básica específica de cada rol hasta el apalancamiento de una comprensión amplia y estratégica que da forma a la dirección organizacional y a las tendencias de la industria. Implica:\n\naplicar conocimientos específicos de roles para realizar tareas rutinarias\nintegrar conocimientos generales, específicos del rol, y de la industria\nutilizar la comprensión de tecnologías, métodos y procesos para lograr resultados\naplicar conocimientos especializados profundos para resolver problemas complejos\naprovechar los amplios conocimientos para influir en las decisiones estratégicas\nconfigurar las prácticas de gestión del conocimiento organizacional.\n\nLa aplicación eficaz del conocimiento se desarrolla a través de la experiencia práctica, la educación formal, la formación profesional, el aprendizaje continuo y la tutoría, y abarca la capacidad de aplicar la comprensión en escenarios del mundo real, adaptarse a los desafíos emergentes y crear valor para la organización.\nA medida que los profesionales avanzan, su aplicación del conocimiento evoluciona considerablemente desde tareas básicas específicas de cada rol al liderazgo estratégico de la organización. Esta progresión implica apoyar las actividades del equipo, aplicar las prácticas dentro de los contextos empresariales, integrar el conocimiento para tareas complejas, ofrecer asesoramiento autorizado y permitir la toma de decisiones entre dominios. A niveles superiores, los profesionales aplican amplios conocimientos empresariales y estratégicos para configurar la estrategia organizativa y anticiparse a las tendencias del sector.", + "levels": [ + { + "level": 1, + "description": "Aplica los conocimientos básicos para realizar tareas rutinarias, bien definidas y predecibles específicas del rol." + }, + { + "level": 2, + "description": "Aplica el conocimiento de las tareas y prácticas comunes del lugar de trabajo para apoyar las actividades del equipo bajo orientación." + }, + { + "level": 3, + "description": "Aplica el conocimiento de una serie de prácticas específicas de roles para completar tareas dentro de límites definidos y tiene una apreciación de cómo este conocimiento se aplica al contexto empresarial más amplio." + }, + { + "level": 4, + "description": "Aplica el conocimiento en diferentes áreas en su campo, integrando este conocimiento para realizar tareas complejas y diversas. Aplica un conocimiento práctico del dominio de la organización." + }, + { + "level": 5, + "description": "Aplica conocimientos para interpretar situaciones complejas y ofrecer asesoramiento autorizado. Aplica una experiencia profunda en campos específicos, con una comprensión más amplia en toda la industria o negocio." + }, + { + "level": 6, + "description": "Aplica amplios conocimientos empresariales para permitir el liderazgo estratégico y la toma de decisiones en diversos ámbitos." + }, + { + "level": 7, + "description": "Aplica conocimientos estratégicos y de base amplia para dar forma a la estrategia organizacional, anticiparse a las tendencias futuras de la industria y preparar a la organización para adaptarse y liderar." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "COLL", + "name": "Colaboración", + "url": "https://sfia-online.org/es/shortcode/9/COLL", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Trabajar eficazmente con otros, compartir recursos y coordinar esfuerzos para alcanzar objetivos compartidos.", + "guidance_notes": "En SFIA, la colaboración representa una progresión de la interacción básica del equipo a las asociaciones estratégicas y la gestión de las partes interesadas. Implica:\n\ntrabajar cooperativamente dentro de los equipos inmediatos\ncompartir información y recursos de manera efectiva\ncoordinar los esfuerzos para alcanzar objetivos comunes\nfacilitar el trabajo interfuncional en equipo\nconstruir relaciones influyentes en toda la organización\nestablecer y gestionar asociaciones estratégicas.\n\nLa colaboración eficaz abarca la comunicación, la adopción de perspectivas y la capacidad de alinear diversos puntos de vista hacia objetivos comunes, así como la creación de un entorno que fomente el intercambio de conocimientos y la resolución colectiva de problemas.\nA medida que los profesionales avanzan, sus habilidades de colaboración evolucionan desde el apoyo a los objetivos del equipo hasta la configuración de la cultura organizacional, el impulso de la innovación y la mejora de la capacidad de la organización para superar desafíos complejos. En los niveles superiores, la colaboración se extiende para influir en la cooperación y las asociaciones de toda la industria.", + "levels": [ + { + "level": 1, + "description": "Trabaja principalmente en sus propias tareas y solo interactúa con su equipo inmediato. Desarrolla una comprensión de cómo su trabajo apoya a otros." + }, + { + "level": 2, + "description": "Comprende la necesidad de colaborar con su equipo y considera las necesidades del usuario/cliente." + }, + { + "level": 3, + "description": "Comprende y colabora en el análisis de las necesidades del usuario/cliente y así lo representa en su trabajo." + }, + { + "level": 4, + "description": "Facilita la colaboración entre partes interesadas que comparten objetivos comunes.  \n\nColabora con los equipos interfuncionales y contribuye en ellos para garantizar que se satisfagan las necesidades de los usuarios y clientes en todo el producto a entregar y el alcance de su labor." + }, + { + "level": 5, + "description": "Facilita la colaboración entre las partes interesadas que tienen diversos objetivos.\n\nGarantiza formas de trabajo colaborativas en todas las etapas del trabajo para satisfacer las necesidades del usuario/cliente.\n\nConstruye relaciones efectivas en toda la organización y con clientes, proveedores y socios." + }, + { + "level": 6, + "description": "Lidera la colaboración con una amplia gama de partes interesadas a través de objetivos contrapuestos dentro de la organización.\n\nEstablece conexiones sólidas e influyentes con contactos internos y externos fundamentales a nivel de dirección superior/líder técnico." + }, + { + "level": 7, + "description": "Impulsa la colaboración, comprometiéndose con las partes interesadas de liderazgo, para asegurar la alineación con la visión y la estrategia corporativas. \n\nConstruye relaciones fuertes e influyentes con clientes, socios y líderes de la industria." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "COMM", + "name": "Comunicación", + "url": "https://sfia-online.org/es/shortcode/9/COMM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Intercambiar información, ideas y puntos de vista con claridad para permitir el entendimiento mutuo y la cooperación.", + "guidance_notes": "En SFIA, la comunicación representa una progresión desde la interacción básica del equipo hasta la influencia compleja en toda la organización y el compromiso externo. Implica:\n\ncomunicarse dentro de los equipos inmediatos\nintercambiar información e ideas claramente\ntener habilidades verbales y escritas, escucha activa, y la capacidad de utilizar herramientas y plataformas de comunicación de forma adecuada\nadaptar el estilo de comunicación a públicos diversos, tanto técnicos como no técnicos\narticular conceptos complejos de manera que permita la toma de decisiones informadas\ninfluir en la estrategia mediante un diálogo eficaz con las principales partes interesadas.\n\nA medida que los profesionales avanzan, sus habilidades de comunicación evolucionan desde el simple intercambio de información dentro de los equipos hasta influir en las decisiones a los niveles más altos de una organización. Esta progresión implica adaptar la comunicación a diferentes públicos, incluidos las principales partes interesadas y socios externos, y dar forma a los resultados estratégicos a través de un diálogo efectivo. En los niveles más altos, los profesionales asumen la responsabilidad de utilizar comunicaciones para impulsar la dirección organizacional y comprometerse con los líderes de la industria para lograr los objetivos de negocio.", + "levels": [ + { + "level": 1, + "description": "Se comunica con el equipo inmediato para comprender y cumplir las tareas asignadas. Observa, escucha y, si se le sugiere, hace preguntas para buscar información o aclarar instrucciones." + }, + { + "level": 2, + "description": "Comunica información familiar con el equipo inmediato y las partes interesadas directamente relacionadas con su función.\n\nEscucha para obtener comprensión y hace preguntas relevantes para aclarar o buscar más información." + }, + { + "level": 3, + "description": "Se comunica con el equipo y las partes interesadas dentro y fuera de la organización, explicando y presentando información de forma clara.\n\nContribuye a una variedad de conversaciones relacionadas con el trabajo y escucha a los demás para comprender y hace preguntas de sondeo relevantes para su función." + }, + { + "level": 4, + "description": "Se comunica tanto con audiencias técnicas como no técnicas, incluidos el equipo y las partes interesadas dentro y fuera de la organización.\n\nSegún sea necesario, toma la iniciativa en la explicación de conceptos complejos para apoyar la toma de decisiones.\n\nEscucha y hace preguntas perspicaces para identificar diferentes perspectivas a fin de aclarar y confirmar la comprensión." + }, + { + "level": 5, + "description": "Comunica claramente y con impacto, articulando información e ideas complejas para audiencias amplias con diferentes puntos de vista.\n\nLidera y fomenta conversaciones para compartir ideas y generar consenso sobre las medidas a tomar." + }, + { + "level": 6, + "description": "Comunica con credibilidad a todos los niveles y en toda la organización a audiencias amplias con objetivos divergentes.\n\nExplica claramente información e ideas complejas, influyendo en la dirección estratégica.\n\nPromueve el intercambio de información en toda la organización." + }, + { + "level": 7, + "description": "Se comunica con audiencias de todos los niveles dentro de su propia organización y se involucra con la industria.\n\nPresenta argumentos e ideas convincentes de forma autoritaria y convincente para alcanzar los objetivos de negocio." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "IMPM", + "name": "Mentalidad de mejora", + "url": "https://sfia-online.org/es/shortcode/9/IMPM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Identificar continuamente oportunidades para perfeccionar las prácticas de trabajo, procesos, productos o servicios para lograr una mayor eficiencia e impacto.", + "guidance_notes": "En SFIA, tener una mentalidad de mejora representa una progresión desde el reconocimiento de oportunidades de mejora hasta el impulso de una cultura de optimización continua. Implica:\n\nIdentificar áreas de mejora en procesos, productos o servicios\nimplementar cambios para mejorar la eficiencia y la eficacia\nevaluar el impacto de las mejoras y perfeccionar los enfoques\nfomentar y apoyar una mentalidad de mejora continua en los demás\nalinear las iniciativas de mejora con los objetivos organizativos\ncultivar una cultura de mejora y optimización continuas\n\nUna mentalidad de mejora implica la búsqueda proactiva de oportunidades para perfeccionar y optimizar las prácticas de trabajo, los procesos, los productos y los servicios, lo que refleja la creciente responsabilidad de identificar, implementar y liderar mejoras en ámbitos de influencia cada vez mayores.A medida que los profesionales avanzan, su enfoque pasa de identificar oportunidades de mejora en sus propias tareas a liderar iniciativas de mejora entre los equipos y la organización. Esta progresión incluye mejorar las prácticas a nivel personal, apoyar a otros al promover una cultura de optimización continua y garantizar que los esfuerzos de mejora se alineen con los objetivos más amplios de la organización. A niveles más altos, los profesionales asumen la responsabilidad de integrar estrategias de mejora continua en toda la organización, impulsando el impacto a largo plazo.", + "levels": [ + { + "level": 1, + "description": "Identifica oportunidades de mejora en las tareas propias. Sugiere mejoras básicas cuando se le solicita." + }, + { + "level": 2, + "description": "Propone ideas para mejorar el área de trabajo propia.\n\nImplementa cambios acordados en las tareas de trabajo asignadas." + }, + { + "level": 3, + "description": "Identifica e implementa mejoras en su propia área de trabajo.\n\nContribuye a las mejoras de procesos a nivel de equipo." + }, + { + "level": 4, + "description": "Alienta y apoya los debates en equipo sobre iniciativas de mejora.\n\nImplementa cambios de procedimiento dentro de un ámbito definido de trabajo." + }, + { + "level": 5, + "description": "Identifica y evalúa mejoras potenciales de productos, prácticas o servicios.\n\nDirige la aplicación de mejoras dentro de su propia área de responsabilidades.\n\nEvalúa la efectividad de los cambios implementados." + }, + { + "level": 6, + "description": "Impulsa iniciativas de mejora que tienen un impacto significativo en la organización.\n\nAlinea las estrategias de mejora con los objetivos organizacionales.\n\nInvolucra a las partes interesadas en los procesos de mejora." + }, + { + "level": 7, + "description": "Define y comunica el enfoque organizacional para la mejora continua.\n\nCultiva una cultura de mejora continua.\n\nEvalúa el impacto de las iniciativas de mejora en el éxito organizacional." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "CRTY", + "name": "Creatividad", + "url": "https://sfia-online.org/es/shortcode/9/CRTY", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Generar y aplicar ideas innovadoras para mejorar los procesos, resolver problemas e impulsar el éxito organizacional.", + "guidance_notes": "En SFIA, la creatividad representa una progresión desde la generación de ideas básicas hasta el impulso de la innovación estratégica. Implica:\n\ngenerar ideas y soluciones novedosas\naplicar un pensamiento innovador para mejorar los procesos\nresolver problemas complejos de forma creativa\nfomentar y facilitar el pensamiento creativo en otros\ndesarrollar una cultura de innovación\nalinear las iniciativas creativas con la estrategia organizativa.\n\nLa creatividad efectiva abarca el pensamiento imaginativo, las habilidades para resolver problemas y los enfoques convencionales desafiantes, y prospera en entornos que fomentan la asunción de riesgos calculados y valoran las ideas innovadoras.\nA medida que los profesionales avanzan, su papel pasa de contribuir a los procesos creativos a inspirar y liderar la innovación a nivel estratégico. Esta evolución subraya la creciente importancia del pensamiento creativo para impulsar el éxito organizacional y superar desafíos complejos en diversas disciplinas.", + "levels": [ + { + "level": 1, + "description": "Participa en la generación de nuevas ideas cuando así se le solicita." + }, + { + "level": 2, + "description": "Aplica el pensamiento creativo para sugerir nuevas formas de abordar una tarea y resolver problemas." + }, + { + "level": 3, + "description": "Aplica y contribuye a las técnicas de pensamiento creativo para aportar nuevas ideas para su propio trabajo y para actividades en equipo." + }, + { + "level": 4, + "description": "Aplica, facilita y desarrolla conceptos de pensamiento creativo y encuentra formas alternativas de enfocar los resultados del equipo." + }, + { + "level": 5, + "description": "Aplica de manera creativa el pensamiento innovador y prácticas de diseño para identificar soluciones que le den valor en beneficio de clientes y partes interesadas." + }, + { + "level": 6, + "description": "Aplica creativamente una amplia gama de nuevas ideas y técnicas de gestión eficaces para lograr resultados que se alinean con la estrategia de la organización." + }, + { + "level": 7, + "description": "Promueve la creatividad y la innovación para impulsar el desarrollo de estrategias que permitan oportunidades comerciales." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "DECM", + "name": "Toma de decisiones", + "url": "https://sfia-online.org/es/shortcode/9/DECM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Aplicar el pensamiento crítico para evaluar opciones, estimar riesgos y seleccionar el curso de acción más adecuado.", + "guidance_notes": "En SFIA, la toma de decisiones representa una progresión desde las elecciones rutinarias hasta las decisiones estratégicas de alto impacto. Implica:\n\nevaluar la información y evaluar los riesgos\nEquilibrar la intuición y la lógica\ncomprender el contexto organizacional\ndeterminar el mejor curso de acción\nasumir la responsabilidad por los resultados.\n\nLa toma de decisiones efectiva incluye habilidades analíticas y de pensamiento crítico, la capacidad de evaluar riesgos y consecuencias, y una comprensión integral del contexto empresarial, así como decidir cuándo escalar los problemas y cómo equilibrar prioridades contradictorias.\nA medida que los profesionales avanzan, su proceso de adopción de decisiones pasa de abordar cuestiones rutinarias a configurar direcciones estratégicas. Al principio, las decisiones se centran en la gestión de tareas o pequeños proyectos. Con el tiempo, la adopción de decisiones se vuelve más compleja, lo que requiere un mayor análisis, evaluación de riesgos y rendición de cuentas por los resultados de alto impacto. A niveles superiores, los profesionales son responsables de tomar decisiones críticas que influyen en la estrategia y el éxito de la organización.", + "levels": [ + { + "level": 1, + "description": "Utiliza poca discreción en la atención de consultas.  \n\nSe espera que busque orientación en situaciones inesperadas." + }, + { + "level": 2, + "description": "Utiliza discreción limitada para resolver problemas o consultas.\n\nDecide cuándo buscar orientación en situaciones inesperadas." + }, + { + "level": 3, + "description": "Utiliza discreción para identificar y responder a problemas complejos relacionados con sus propias asignaciones. \n\nDetermina cuándo los problemas deben escalarse a un nivel superior." + }, + { + "level": 4, + "description": "Utiliza su criterio y una discreción sustancial para identificar y responder a problemas complejos y tareas relacionadas con proyectos y objetivos del equipo.\n\nEscala cuando el alcance se ve afectado." + }, + { + "level": 5, + "description": "Utiliza el buen juicio para tomar decisiones informadas sobre las acciones para lograr resultados organizacionales, como el cumplimiento de metas, plazos y presupuestos.\n\nPlantea cuestionamientos cuando los objetivos están en riesgo." + }, + { + "level": 6, + "description": "Utiliza su criterio para tomar decisiones que inician el logro de los objetivos estratégicos acordados, incluido el rendimiento financiero.\n\nEscala cuando se ve afectada una dirección estratégica más amplia." + }, + { + "level": 7, + "description": "Aplica el buen juicio en la toma de decisiones críticas para la dirección estratégica de la organización y el éxito.\n\nEscala cuando se requiere la aportación de la dirección ejecutiva empresarial a través de estructuras de gobernanza establecidas." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "DIGI", + "name": "Mentalidad digital", + "url": "https://sfia-online.org/es/shortcode/9/DIGI", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Adoptar y utilizar eficazmente las herramientas y tecnologías digitales para mejorar el rendimiento y la productividad.", + "guidance_notes": "En SFIA, tener una mentalidad digital representa una progresión desde la alfabetización digital básica hasta impulsar la estrategia digital organizacional. Implica:\n\nentender y aplicar las tecnologías digitales\nadaptarse a panoramas digitales en rápida evolución\nutilizar herramientas digitales, IA y datos para potenciar los procesos de trabajo\nimpulsar la innovación y la transformación digital\ncomprender las implicaciones de las tecnologías emergentes, incluida la IA, y su potencial para impulsar el cambio organizacional\ngarantizar la gobernanza y el cumplimiento digital.\n\nUna mentalidad digital eficaz abarca el aprendizaje continuo, la adaptabilidad y la capacidad de ver cómo las tecnologías digitales pueden transformar los modelos y estrategias de negocio. También implica comprender las implicaciones de las tecnologías emergentes y su potencial para impulsar el cambio organizacional.\nA medida que los profesionales avanzan, su mentalidad digital evoluciona desde el simple uso de herramientas digitales hasta dar forma y liderar las estrategias digitales de la organización. Al principio de sus carreras, se centran en aplicar las habilidades digitales a sus funciones, pero a medida que avanzan, comienzan a impulsar la innovación y a utilizar las tecnologías emergentes para transformar los procesos de trabajo. En los niveles superiores, los profesionales son responsables de liderar la transformación digital, garantizar el cumplimiento de la gobernanza digital e incorporar una cultura digital en toda la organización.", + "levels": [ + { + "level": 1, + "description": "Tiene habilidades digitales básicas para aprender y utilizar aplicaciones, procesos y herramientas para su función." + }, + { + "level": 2, + "description": "Tiene habilidades digitales suficientes para su rol; comprende y utiliza métodos, herramientas, aplicaciones y procesos apropiados." + }, + { + "level": 3, + "description": "Explora y aplica herramientas y habilidades digitales relevantes para su función.\n\nComprende y aplica de manera efectiva métodos, herramientas, aplicaciones y procesos adecuados." + }, + { + "level": 4, + "description": "Maximiza las capacidades de las aplicaciones para su función, y evalúa y soporta el uso de nuevas tecnologías y herramientas digitales.\n\nSelecciona adecuadamente y evalúa el impacto de los cambios en los estándares, métodos, herramientas, aplicaciones y procesos aplicables y relevantes para la propia especialidad." + }, + { + "level": 5, + "description": "Reconoce y evalúa el impacto organizacional de las nuevas tecnologías y servicios digitales.\n\nImplementa prácticas nuevas y efectivas. \n\nAsesora sobre los estándares, métodos, herramientas, aplicaciones y procesos disponibles y relevantes para especialidades grupales, y puede tomar decisiones apropiadas a partir de las alternativas." + }, + { + "level": 6, + "description": "Lidera la mejora de las capacidades digitales de la organización. \n\nIdentifica y respalda oportunidades para adoptar nuevas tecnologías y servicios digitales.\n\nLidera la gobernanza digital y el cumplimiento de la legislación pertinente, y la necesidad de productos y servicios." + }, + { + "level": 7, + "description": "Lidera el desarrollo de la cultura digital de la organización y la visión transformadora.  \n\nPromueve la capacidad y/o la explotación de la tecnología dentro de una o más organizaciones a través de una comprensión profunda de la industria y las implicaciones de las tecnologías emergentes.\n\nEs responsable de evaluar cómo las leyes y reglamentaciones impactan los objetivos organizacionales y el uso de las capacidades digitales, de datos y tecnología." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "LEAD", + "name": "Liderazgo", + "url": "https://sfia-online.org/es/shortcode/9/LEAD", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Guiar e influir en las personas o equipos para alinear las acciones con los objetivos estratégicos e impulsar resultados positivos.", + "guidance_notes": "En SFIA, el liderazgo representa una progresión desde la autogestión hasta una configuración de la estrategia organizacional. Implica:\n\ndemostrar responsabilidad personal\nhacer propio el trabajo y el desarrollo\nguiar e influir en los demás\ncontribuir a las capacidades del equipo\nalinear las acciones con los objetivos organizacionales\ninspirar e impulsar un cambio positivo.\n\nEl liderazgo efectivo abarca la autoconciencia, la influencia, la comprensión y la inspiración y motivación de los demás. También implica el pensamiento estratégico, la gestión de riesgos y la capacidad de alinear las acciones con los objetivos a largo plazo.\nA medida que los profesionales avanzan, su liderazgo evoluciona desde la gestión de las responsabilidades personales hasta la orientación de los equipos y, finalmente, a la configuración de la estrategia organizacional. Con el tiempo, van más allá de influir en los equipos para impulsar resultados estratégicos, alinear las políticas con los objetivos de la organización y gestionar los riesgos a una escala más amplia. En los niveles superiores, el liderazgo desempeña un papel fundamental en la configuración de la cultura organizacional, el impulso de la innovación y la mejora de la capacidad de la organización para superar desafíos complejos y aprovechar las oportunidades.", + "levels": [ + { + "level": 1, + "description": "Aumenta proactivamente su comprensión de sus tareas y responsabilidades laborales." + }, + { + "level": 2, + "description": "Se hace cargo de desarrollar su experiencia laboral." + }, + { + "level": 3, + "description": "Proporciona orientación básica y apoyo a los miembros menos experimentados del equipo según sea necesario." + }, + { + "level": 4, + "description": "Dirige, apoya o guía a los miembros del equipo.\n\nDesarrolla soluciones para actividades laborales complejas relacionadas con asignaciones. \n\nDemuestra una comprensión de los factores de riesgo en su trabajo.\n\nContribuye con conocimientos especializados a la definición de requisitos para respaldar propuestas." + }, + { + "level": 5, + "description": "Proporciona liderazgo a nivel operativo.\n\nImplementa y ejecuta políticas alineadas con los planes estratégicos.\n\nEstima y evalúa riesgos.\n\nAl examinar propuestas toma en cuenta todos los requisitos." + }, + { + "level": 6, + "description": "Proporciona liderazgo a nivel organizacional.\n\nContribuye al desarrollo e implementación de políticas y estrategias.\n\nEntiende y comunica la evolución de la industria, y el rol y el impacto de la tecnología. \n\nGestiona y mitiga los riesgos organizacionales.  \n\nEquilibra los requisitos de las propuestas con las necesidades más generales de la organización." + }, + { + "level": 7, + "description": "Lidera la gestión estratégica.\n\nAplica el más alto nivel de liderazgo a la formulación e implementación de la estrategia.\n\nComunica el impacto potencial de prácticas y tecnologías emergentes en organizaciones e individuos y evalúa los riesgos de usar o no tales prácticas y tecnologías. \n\nEstablece la gobernanza para abordar los riesgos empresariales.\n\nAsegura que las propuestas se alineen con la dirección estratégica de la organización." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "url": "https://sfia-online.org/es/shortcode/9/LADV", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Adquirir continuamente nuevos conocimientos y habilidades para mejorar el rendimiento personal y organizacional.", + "guidance_notes": "En SFIA, el aprendizaje y el desarrollo profesional representan una progresión desde la mejora de las habilidades personales hasta la configuración de la cultura de aprendizaje organizacional. Implica:\n\nadquirir y aplicar nuevos conocimientos\nidentificar y subsanar las deficiencias de aptitudes\ncompartir aprendizajes con colegas\nimpulsar el desarrollo personal y del equipo\npromover la aplicación del conocimiento para objetivos estratégicos\ninspirar una cultura de aprendizaje alineada con los objetivos de negocio.\n\nEl aprendizaje y el desarrollo profesional eficaces abarcan la educación formal, el aprendizaje experiencial, el estudio autodirigido y la capacidad de evaluar y aplicar de manera crítica nueva información. También implica mantener la conciencia de las prácticas emergentes y las tendencias de la industria, y alinear las iniciativas de aprendizaje con los objetivos estratégicos del negocio.A medida que los profesionales avanzan, su enfoque del aprendizaje y el desarrollo evoluciona desde centrarse en la mejora de las habilidades personales hasta impulsar el desarrollo del equipo y de la organización. Con el tiempo, pasan de aplicar nuevos conocimientos a liderar esfuerzos que moldean una cultura de aprendizaje, alineando las iniciativas de desarrollo con los objetivos estratégicos. En los niveles superiores, los profesionales no solo inspiran una cultura de aprendizaje, sino que también garantizan que la organización tenga las habilidades y capacidades necesarias para enfrentar los cambios de la industria y aprovechar las oportunidades.", + "levels": [ + { + "level": 1, + "description": "Aplica los conocimientos recién adquiridos para desarrollar habilidades para su rol. Contribuye a identificar las propias oportunidades de desarrollo." + }, + { + "level": 2, + "description": "Absorbe y aplica nueva información a las tareas.\n\nReconoce las habilidades personales y las brechas de conocimiento y busca oportunidades de aprendizaje para abordarlas." + }, + { + "level": 3, + "description": "Absorbe y aplica información nueva de manera efectiva con la capacidad de compartir aprendizajes con colegas.\n\nToma la iniciativa para identificar y negociar sus propias oportunidades de desarrollo apropiadas." + }, + { + "level": 4, + "description": "Absorbe rápidamente y evalúa críticamente nueva información y la aplica eficazmente.\n\nMantiene una comprensión de las prácticas emergentes y de su aplicación, y asume la responsabilidad de impulsar las oportunidades de desarrollo propias y de los miembros del equipo." + }, + { + "level": 5, + "description": "Utiliza sus habilidades y conocimientos para ayudar a establecer los estándares que otros en la organización aplicarán.\n\nToma la iniciativa de desarrollar una mayor amplitud de conocimientos en toda la industria y/o negocio, así como para identificar y gestionar oportunidades de desarrollo en su área de responsabilidad." + }, + { + "level": 6, + "description": "Promueve la aplicación del conocimiento para apoyar imperativos estratégicos.\n\nDesarrolla activamente sus habilidades de liderazgo estratégico y técnico, y lidera el desarrollo de habilidades en su área de responsabilidad." + }, + { + "level": 7, + "description": "Inspira una cultura de aprendizaje para alinearse con los objetivos de negocio.   \n\nMantiene una visión estratégica de los panoramas contemporáneos y emergentes de la industria. \n\nSe asegura de que la organización desarrolle y movilice toda la gama de capacidades y habilidades requeridas." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "PLAN", + "name": "Planificación", + "url": "https://sfia-online.org/es/shortcode/9/PLAN", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Adoptar un enfoque sistemático para organizar las tareas, los recursos y los plazos a fin de alcanzar los objetivos definidos.", + "guidance_notes": "La planificación en SFIA representa una progresión desde la organización del trabajo individual hasta la planificación estratégica líder en toda la organización. Implica: \n\nfijar objetivos y determinar plazos\norganizar las tareas y asignar los recursos\nalinear las actividades con metas más grandes\nadaptar los planes a las circunstancias cambiantes\nhacer un seguimiento de los progresos y evaluar los resultados\niniciar e influir en los objetivos estratégicos.\n\nLa planificación eficaz abarca las habilidades analíticas, la previsión y la capacidad de equilibrar múltiples prioridades. También implica la adaptabilidad para responder a circunstancias cambiantes, y la capacidad de alinear los planes operativos con los objetivos estratégicos. A medida que los profesionales avanzan, sus habilidades de planificación moldean cada vez más la dirección y el rendimiento organizacional.\nA medida que los profesionales avanzan, sus responsabilidades de planificación pasan de gestionar tareas personales o de equipo a impulsar los esfuerzos de planificación organizacional. Con el tiempo, pasan de organizar su propio trabajo a establecer objetivos estratégicos que dan forma a la dirección de la organización. En los niveles superiores, los profesionales lideran la planificación de iniciativas complejas, garantizando la alineación con los objetivos estratégicos y orientando el rendimiento organizacional.", + "levels": [ + { + "level": 1, + "description": "Confirma los pasos necesarios para las tareas individuales." + }, + { + "level": 2, + "description": "Planifica su propio trabajo en cortos horizontes temporales de forma organizada." + }, + { + "level": 3, + "description": "Organiza y realiza un seguimiento de su propio trabajo (y de otros, cuando sea necesario) para cumplir los plazos acordados." + }, + { + "level": 4, + "description": "Planifica, programa y supervisa el trabajo para cumplir determinados objetivos y procesos personales y/o de equipo, demostrando un enfoque analítico para cumplir alcanzar los objetivos de tiempo y calidad." + }, + { + "level": 5, + "description": "Analiza, diseña, planifica, establece hitos y ejecuta y evalúa el trabajo según los objetivos de tiempo, costo y calidad." + }, + { + "level": 6, + "description": "Inicia e influye en los objetivos estratégicos y asigna responsabilidades." + }, + { + "level": 7, + "description": "Planifica y lidera al más alto nivel de autoridad sobre todos los aspectos de un área de trabajo considerable." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "PROB", + "name": "Resolución de problemas", + "url": "https://sfia-online.org/es/shortcode/9/PROB", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Analizar retos, aplicar métodos lógicos y desarrollar soluciones eficaces para superar los obstáculos.", + "guidance_notes": "En SFIA, la resolución de problemas representa una progresión, desde abordar problemas rutinarios hasta gestionar desafíos estratégicos. Implica:\n\nreconocer y comprender los problemas\nanalizar posibles soluciones\naplicar resoluciones eficaces\nevaluar los resultados y aprender de las experiencias\nanticipar y abordar de manera proactiva los posibles problemas\nalinear la resolución de problemas con los objetivos organizacionales.\n\nLa solución eficaz de problemas incluye el pensamiento analítico, la creatividad y la capacidad de tomar decisiones informadas. También implica la colaboración con expertos de diversas disciplinas, en particular en los niveles superiores.\nA medida que los profesionales avanzan, sus responsabilidades en la solución de problemas pasan de resolver problemas rutinarios a abordar desafíos complejos y estratégicos. Inicialmente, se centran en enfoques metódicos de los problemas cotidianos, pero con el tiempo desarrollan la capacidad de anticiparse a los problemas, evaluar una variedad de soluciones y abordar desafíos que afectan objetivos organizacionales más amplios. En los niveles superiores, los profesionales lideran las actividades de resolución de problemas para asegurarse de que los desafíos complejos se administren en consonancia con los objetivos a largo plazo.", + "levels": [ + { + "level": 1, + "description": "Trabaja para entender el problema y busca asistencia para resolver problemas inesperados." + }, + { + "level": 2, + "description": "Investiga y resuelve problemas rutinarios." + }, + { + "level": 3, + "description": "Aplica un enfoque metódico para investigar y evaluar opciones para resolver problemas rutinarios y moderadamente complejos." + }, + { + "level": 4, + "description": "Investiga la causa y el impacto, evalúa las opciones y resuelve una amplia gama de problemas complejos." + }, + { + "level": 5, + "description": "Investiga cuestiones complejas para identificar las causas fundamentales y los impactos, evalúa una variedad de soluciones y toma decisiones informadas sobre el mejor curso de acción, a menudo en colaboración con otros expertos." + }, + { + "level": 6, + "description": "Anticipa y lidera la resolución de problemas y oportunidades que puedan afectar los objetivos de la organización, estableciendo un enfoque estratégico y asignando recursos." + }, + { + "level": 7, + "description": "Gestiona las interrelaciones entre las partes afectadas y los imperativos estratégicos, reconociendo el contexto empresarial más amplio y sacando conclusiones precisas al resolver problemas." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "ADAP", + "name": "Adaptabilidad", + "url": "https://sfia-online.org/es/shortcode/9/ADAP", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Ajustarse al cambio y persistir a través de desafíos a nivel personal, del equipo y de la organización.", + "guidance_notes": "En SFIA, la adaptabilidad y la resiliencia representan una progresión de la flexibilidad personal a la configuración de la agilidad organizacional. Implica:\n\nestar abiertos al cambio y a nuevas formas de trabajar\nadaptarse a las diferentes dinámicas de equipo y requisitos de trabajo\nadoptar nuevos métodos y tecnologías de forma proactiva\npermitir a otros adaptarse a los desafíos\nliderar equipos a través de transiciones\nimpulsar cambios organizativos considerables\nintegrar la adaptabilidad en la cultura organizacional.\n\nLa adaptabilidad y la resiliencia efectivas abarcan la apertura al cambio, el aprendizaje proactivo y la capacidad de mantener el enfoque en los objetivos durante las transiciones. También implica apoyar a otros a través del cambio y la creación de un entorno donde prosperen la innovación y la flexibilidad.\nA medida que los profesionales avanzan, su capacidad para impulsar y gestionar el cambio moldea cada vez más la resiliencia organizacional y el éxito a largo plazo en entornos dinámicos.", + "levels": [ + { + "level": 1, + "description": "Acepta el cambio y está abierto a nuevas formas de trabajar." + }, + { + "level": 2, + "description": "Se ajusta a diferentes dinámicas de equipo y requisitos de trabajo.\n\nParticipa en procesos de adaptación de equipos." + }, + { + "level": 3, + "description": "Se adapta y responde al cambio y muestra iniciativa en la adopción de nuevos métodos o tecnologías." + }, + { + "level": 4, + "description": "Permite a otros adaptarse y cambiar en respuesta a los desafíos y cambios en el entorno de trabajo." + }, + { + "level": 5, + "description": "Lidera adaptaciones a entornos empresariales cambiantes.\n\nGuía a los equipos a través de las transiciones, manteniendo el enfoque en los objetivos organizacionales." + }, + { + "level": 6, + "description": "Impulsa la adaptabilidad organizacional iniciando y liderando cambios significativos. Influye en las estrategias de gestión del cambio a nivel organizacional." + }, + { + "level": 7, + "description": "Promueve la agilidad organizativa y la resiliencia.\n\nIntegra la adaptabilidad en la cultura organizacional y la planificación estratégica." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "es", + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "url": "https://sfia-online.org/es/shortcode/9/SCPE", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Garantizar la protección de la información confidencial, mantener la privacidad de los datos y las personas, y demostrar una conducta ética dentro y fuera de la organización.", + "guidance_notes": "En SFIA, la seguridad, la privacidad y la ética representan una progresión desde la conciencia básica hasta el liderazgo estratégico. Implica:\n\naplicar prácticas de trabajo profesionales y respetar las normas organizativas\nimplementar normas y mejores prácticas\npromover una cultura de seguridad, privacidad y conducta ética\nabordar los desafíos éticos, incluidos los introducidos por las tecnologías emergentes, como la IA\ngarantizar el cumplimiento de las leyes y los reglamentos pertinentes\nliderar iniciativas que incorporen la seguridad, la privacidad y la ética a la cultura y las operaciones de la organización.\n\nLa gestión eficaz de la seguridad, la privacidad y la ética abarca los conocimientos técnicos, las habilidades éticas para adoptar decisiones y la capacidad de equilibrar prioridades contrapuestas. También implica crear un entorno en el que estos principios estén integrados en todos los aspectos del trabajo.\nA medida que los profesionales avanzan, se espera que asuman un papel activo en la promoción del comportamiento ético y la obtención de información sensible en todas las áreas de trabajo. En los niveles superiores, las personas son responsables de desarrollar estrategias que equilibren las necesidades operativas con las consideraciones éticas, asegurando la sostenibilidad y la confianza a largo plazo.", + "levels": [ + { + "level": 1, + "description": "Desarrolla una comprensión de las reglas y expectativas de su función y de la organización." + }, + { + "level": 2, + "description": "Tiene una buena comprensión de su papel y de las reglas y expectativas de la organización." + }, + { + "level": 3, + "description": "Aplica el profesionalismo adecuado y las prácticas y conocimientos prácticos para trabajar." + }, + { + "level": 4, + "description": "Adapta y aplica las normas aplicables, reconociendo su importancia en la consecución de resultados en equipo." + }, + { + "level": 5, + "description": "Contribuye de manera proactiva a la implementación de prácticas de trabajo profesionales y ayuda a promover una cultura organizacional de apoyo." + }, + { + "level": 6, + "description": "Desempeña un papel de liderazgo en la promoción y garantía de una cultura y prácticas de trabajo adecuadas, incluida la igualdad de acceso y oportunidades para las personas con capacidades diversas." + }, + { + "level": 7, + "description": "Proporciona una dirección clara y un liderazgo estratégico para integrar el cumplimiento, la cultura organizacional y las prácticas de trabajo, y promueve activamente la diversidad y la inclusión." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "sheet": "Atributos", + "source_date": null + } + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/es/behaviour-matrix.json b/projects/odilo/data/sfia-9-json/es/behaviour-matrix.json new file mode 100644 index 000000000..03cdfc1ce --- /dev/null +++ b/projects/odilo/data/sfia-9-json/es/behaviour-matrix.json @@ -0,0 +1,895 @@ +{ + "type": "sfia.behaviour_matrix", + "sfia_version": 9, + "language": "es", + "factors": [ + { + "code": "COLL", + "name": "Colaboración", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration", + "levels": [ + { + "level": 1, + "description": "Trabaja principalmente en sus propias tareas y solo interactúa con su equipo inmediato. Desarrolla una comprensión de cómo su trabajo apoya a otros." + }, + { + "level": 2, + "description": "Comprende la necesidad de colaborar con su equipo y considera las necesidades del usuario/cliente." + }, + { + "level": 3, + "description": "Comprende y colabora en el análisis de las necesidades del usuario/cliente y así lo representa en su trabajo." + }, + { + "level": 4, + "description": "Facilita la colaboración entre partes interesadas que comparten objetivos comunes.  \n\nColabora con los equipos interfuncionales y contribuye en ellos para garantizar que se satisfagan las necesidades de los usuarios y clientes en todo el producto a entregar y el alcance de su labor." + }, + { + "level": 5, + "description": "Facilita la colaboración entre las partes interesadas que tienen diversos objetivos.\n\nGarantiza formas de trabajo colaborativas en todas las etapas del trabajo para satisfacer las necesidades del usuario/cliente.\n\nConstruye relaciones efectivas en toda la organización y con clientes, proveedores y socios." + }, + { + "level": 6, + "description": "Lidera la colaboración con una amplia gama de partes interesadas a través de objetivos contrapuestos dentro de la organización.\n\nEstablece conexiones sólidas e influyentes con contactos internos y externos fundamentales a nivel de dirección superior/líder técnico." + }, + { + "level": 7, + "description": "Impulsa la colaboración, comprometiéndose con las partes interesadas de liderazgo, para asegurar la alineación con la visión y la estrategia corporativas. \n\nConstruye relaciones fuertes e influyentes con clientes, socios y líderes de la industria." + } + ] + }, + { + "code": "COMM", + "name": "Comunicación", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication", + "levels": [ + { + "level": 1, + "description": "Se comunica con el equipo inmediato para comprender y cumplir las tareas asignadas. Observa, escucha y, si se le sugiere, hace preguntas para buscar información o aclarar instrucciones." + }, + { + "level": 2, + "description": "Comunica información familiar con el equipo inmediato y las partes interesadas directamente relacionadas con su función.\n\nEscucha para obtener comprensión y hace preguntas relevantes para aclarar o buscar más información." + }, + { + "level": 3, + "description": "Se comunica con el equipo y las partes interesadas dentro y fuera de la organización, explicando y presentando información de forma clara.\n\nContribuye a una variedad de conversaciones relacionadas con el trabajo y escucha a los demás para comprender y hace preguntas de sondeo relevantes para su función." + }, + { + "level": 4, + "description": "Se comunica tanto con audiencias técnicas como no técnicas, incluidos el equipo y las partes interesadas dentro y fuera de la organización.\n\nSegún sea necesario, toma la iniciativa en la explicación de conceptos complejos para apoyar la toma de decisiones.\n\nEscucha y hace preguntas perspicaces para identificar diferentes perspectivas a fin de aclarar y confirmar la comprensión." + }, + { + "level": 5, + "description": "Comunica claramente y con impacto, articulando información e ideas complejas para audiencias amplias con diferentes puntos de vista.\n\nLidera y fomenta conversaciones para compartir ideas y generar consenso sobre las medidas a tomar." + }, + { + "level": 6, + "description": "Comunica con credibilidad a todos los niveles y en toda la organización a audiencias amplias con objetivos divergentes.\n\nExplica claramente información e ideas complejas, influyendo en la dirección estratégica.\n\nPromueve el intercambio de información en toda la organización." + }, + { + "level": 7, + "description": "Se comunica con audiencias de todos los niveles dentro de su propia organización y se involucra con la industria.\n\nPresenta argumentos e ideas convincentes de forma autoritaria y convincente para alcanzar los objetivos de negocio." + } + ] + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement", + "levels": [ + { + "level": 1, + "description": "Identifica oportunidades de mejora en las tareas propias. Sugiere mejoras básicas cuando se le solicita." + }, + { + "level": 2, + "description": "Propone ideas para mejorar el área de trabajo propia.\n\nImplementa cambios acordados en las tareas de trabajo asignadas." + }, + { + "level": 3, + "description": "Identifica e implementa mejoras en su propia área de trabajo.\n\nContribuye a las mejoras de procesos a nivel de equipo." + }, + { + "level": 4, + "description": "Alienta y apoya los debates en equipo sobre iniciativas de mejora.\n\nImplementa cambios de procedimiento dentro de un ámbito definido de trabajo." + }, + { + "level": 5, + "description": "Identifica y evalúa mejoras potenciales de productos, prácticas o servicios.\n\nDirige la aplicación de mejoras dentro de su propia área de responsabilidades.\n\nEvalúa la efectividad de los cambios implementados." + }, + { + "level": 6, + "description": "Impulsa iniciativas de mejora que tienen un impacto significativo en la organización.\n\nAlinea las estrategias de mejora con los objetivos organizacionales.\n\nInvolucra a las partes interesadas en los procesos de mejora." + }, + { + "level": 7, + "description": "Define y comunica el enfoque organizacional para la mejora continua.\n\nCultiva una cultura de mejora continua.\n\nEvalúa el impacto de las iniciativas de mejora en el éxito organizacional." + } + ] + }, + { + "code": "CRTY", + "name": "Creatividad", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity", + "levels": [ + { + "level": 1, + "description": "Participa en la generación de nuevas ideas cuando así se le solicita." + }, + { + "level": 2, + "description": "Aplica el pensamiento creativo para sugerir nuevas formas de abordar una tarea y resolver problemas." + }, + { + "level": 3, + "description": "Aplica y contribuye a las técnicas de pensamiento creativo para aportar nuevas ideas para su propio trabajo y para actividades en equipo." + }, + { + "level": 4, + "description": "Aplica, facilita y desarrolla conceptos de pensamiento creativo y encuentra formas alternativas de enfocar los resultados del equipo." + }, + { + "level": 5, + "description": "Aplica de manera creativa el pensamiento innovador y prácticas de diseño para identificar soluciones que le den valor en beneficio de clientes y partes interesadas." + }, + { + "level": 6, + "description": "Aplica creativamente una amplia gama de nuevas ideas y técnicas de gestión eficaces para lograr resultados que se alinean con la estrategia de la organización." + }, + { + "level": 7, + "description": "Promueve la creatividad y la innovación para impulsar el desarrollo de estrategias que permitan oportunidades comerciales." + } + ] + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision", + "levels": [ + { + "level": 1, + "description": "Utiliza poca discreción en la atención de consultas.  \n\nSe espera que busque orientación en situaciones inesperadas." + }, + { + "level": 2, + "description": "Utiliza discreción limitada para resolver problemas o consultas.\n\nDecide cuándo buscar orientación en situaciones inesperadas." + }, + { + "level": 3, + "description": "Utiliza discreción para identificar y responder a problemas complejos relacionados con sus propias asignaciones. \n\nDetermina cuándo los problemas deben escalarse a un nivel superior." + }, + { + "level": 4, + "description": "Utiliza su criterio y una discreción sustancial para identificar y responder a problemas complejos y tareas relacionadas con proyectos y objetivos del equipo.\n\nEscala cuando el alcance se ve afectado." + }, + { + "level": 5, + "description": "Utiliza el buen juicio para tomar decisiones informadas sobre las acciones para lograr resultados organizacionales, como el cumplimiento de metas, plazos y presupuestos.\n\nPlantea cuestionamientos cuando los objetivos están en riesgo." + }, + { + "level": 6, + "description": "Utiliza su criterio para tomar decisiones que inician el logro de los objetivos estratégicos acordados, incluido el rendimiento financiero.\n\nEscala cuando se ve afectada una dirección estratégica más amplia." + }, + { + "level": 7, + "description": "Aplica el buen juicio en la toma de decisiones críticas para la dirección estratégica de la organización y el éxito.\n\nEscala cuando se requiere la aportación de la dirección ejecutiva empresarial a través de estructuras de gobernanza establecidas." + } + ] + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset", + "levels": [ + { + "level": 1, + "description": "Tiene habilidades digitales básicas para aprender y utilizar aplicaciones, procesos y herramientas para su función." + }, + { + "level": 2, + "description": "Tiene habilidades digitales suficientes para su rol; comprende y utiliza métodos, herramientas, aplicaciones y procesos apropiados." + }, + { + "level": 3, + "description": "Explora y aplica herramientas y habilidades digitales relevantes para su función.\n\nComprende y aplica de manera efectiva métodos, herramientas, aplicaciones y procesos adecuados." + }, + { + "level": 4, + "description": "Maximiza las capacidades de las aplicaciones para su función, y evalúa y soporta el uso de nuevas tecnologías y herramientas digitales.\n\nSelecciona adecuadamente y evalúa el impacto de los cambios en los estándares, métodos, herramientas, aplicaciones y procesos aplicables y relevantes para la propia especialidad." + }, + { + "level": 5, + "description": "Reconoce y evalúa el impacto organizacional de las nuevas tecnologías y servicios digitales.\n\nImplementa prácticas nuevas y efectivas. \n\nAsesora sobre los estándares, métodos, herramientas, aplicaciones y procesos disponibles y relevantes para especialidades grupales, y puede tomar decisiones apropiadas a partir de las alternativas." + }, + { + "level": 6, + "description": "Lidera la mejora de las capacidades digitales de la organización. \n\nIdentifica y respalda oportunidades para adoptar nuevas tecnologías y servicios digitales.\n\nLidera la gobernanza digital y el cumplimiento de la legislación pertinente, y la necesidad de productos y servicios." + }, + { + "level": 7, + "description": "Lidera el desarrollo de la cultura digital de la organización y la visión transformadora.  \n\nPromueve la capacidad y/o la explotación de la tecnología dentro de una o más organizaciones a través de una comprensión profunda de la industria y las implicaciones de las tecnologías emergentes.\n\nEs responsable de evaluar cómo las leyes y reglamentaciones impactan los objetivos organizacionales y el uso de las capacidades digitales, de datos y tecnología." + } + ] + }, + { + "code": "LEAD", + "name": "Liderazgo", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership", + "levels": [ + { + "level": 1, + "description": "Aumenta proactivamente su comprensión de sus tareas y responsabilidades laborales." + }, + { + "level": 2, + "description": "Se hace cargo de desarrollar su experiencia laboral." + }, + { + "level": 3, + "description": "Proporciona orientación básica y apoyo a los miembros menos experimentados del equipo según sea necesario." + }, + { + "level": 4, + "description": "Dirige, apoya o guía a los miembros del equipo.\n\nDesarrolla soluciones para actividades laborales complejas relacionadas con asignaciones. \n\nDemuestra una comprensión de los factores de riesgo en su trabajo.\n\nContribuye con conocimientos especializados a la definición de requisitos para respaldar propuestas." + }, + { + "level": 5, + "description": "Proporciona liderazgo a nivel operativo.\n\nImplementa y ejecuta políticas alineadas con los planes estratégicos.\n\nEstima y evalúa riesgos.\n\nAl examinar propuestas toma en cuenta todos los requisitos." + }, + { + "level": 6, + "description": "Proporciona liderazgo a nivel organizacional.\n\nContribuye al desarrollo e implementación de políticas y estrategias.\n\nEntiende y comunica la evolución de la industria, y el rol y el impacto de la tecnología. \n\nGestiona y mitiga los riesgos organizacionales.  \n\nEquilibra los requisitos de las propuestas con las necesidades más generales de la organización." + }, + { + "level": 7, + "description": "Lidera la gestión estratégica.\n\nAplica el más alto nivel de liderazgo a la formulación e implementación de la estrategia.\n\nComunica el impacto potencial de prácticas y tecnologías emergentes en organizaciones e individuos y evalúa los riesgos de usar o no tales prácticas y tecnologías. \n\nEstablece la gobernanza para abordar los riesgos empresariales.\n\nAsegura que las propuestas se alineen con la dirección estratégica de la organización." + } + ] + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning", + "levels": [ + { + "level": 1, + "description": "Aplica los conocimientos recién adquiridos para desarrollar habilidades para su rol. Contribuye a identificar las propias oportunidades de desarrollo." + }, + { + "level": 2, + "description": "Absorbe y aplica nueva información a las tareas.\n\nReconoce las habilidades personales y las brechas de conocimiento y busca oportunidades de aprendizaje para abordarlas." + }, + { + "level": 3, + "description": "Absorbe y aplica información nueva de manera efectiva con la capacidad de compartir aprendizajes con colegas.\n\nToma la iniciativa para identificar y negociar sus propias oportunidades de desarrollo apropiadas." + }, + { + "level": 4, + "description": "Absorbe rápidamente y evalúa críticamente nueva información y la aplica eficazmente.\n\nMantiene una comprensión de las prácticas emergentes y de su aplicación, y asume la responsabilidad de impulsar las oportunidades de desarrollo propias y de los miembros del equipo." + }, + { + "level": 5, + "description": "Utiliza sus habilidades y conocimientos para ayudar a establecer los estándares que otros en la organización aplicarán.\n\nToma la iniciativa de desarrollar una mayor amplitud de conocimientos en toda la industria y/o negocio, así como para identificar y gestionar oportunidades de desarrollo en su área de responsabilidad." + }, + { + "level": 6, + "description": "Promueve la aplicación del conocimiento para apoyar imperativos estratégicos.\n\nDesarrolla activamente sus habilidades de liderazgo estratégico y técnico, y lidera el desarrollo de habilidades en su área de responsabilidad." + }, + { + "level": 7, + "description": "Inspira una cultura de aprendizaje para alinearse con los objetivos de negocio.   \n\nMantiene una visión estratégica de los panoramas contemporáneos y emergentes de la industria. \n\nSe asegura de que la organización desarrolle y movilice toda la gama de capacidades y habilidades requeridas." + } + ] + }, + { + "code": "PLAN", + "name": "Planificación", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning", + "levels": [ + { + "level": 1, + "description": "Confirma los pasos necesarios para las tareas individuales." + }, + { + "level": 2, + "description": "Planifica su propio trabajo en cortos horizontes temporales de forma organizada." + }, + { + "level": 3, + "description": "Organiza y realiza un seguimiento de su propio trabajo (y de otros, cuando sea necesario) para cumplir los plazos acordados." + }, + { + "level": 4, + "description": "Planifica, programa y supervisa el trabajo para cumplir determinados objetivos y procesos personales y/o de equipo, demostrando un enfoque analítico para cumplir alcanzar los objetivos de tiempo y calidad." + }, + { + "level": 5, + "description": "Analiza, diseña, planifica, establece hitos y ejecuta y evalúa el trabajo según los objetivos de tiempo, costo y calidad." + }, + { + "level": 6, + "description": "Inicia e influye en los objetivos estratégicos y asigna responsabilidades." + }, + { + "level": 7, + "description": "Planifica y lidera al más alto nivel de autoridad sobre todos los aspectos de un área de trabajo considerable." + } + ] + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem", + "levels": [ + { + "level": 1, + "description": "Trabaja para entender el problema y busca asistencia para resolver problemas inesperados." + }, + { + "level": 2, + "description": "Investiga y resuelve problemas rutinarios." + }, + { + "level": 3, + "description": "Aplica un enfoque metódico para investigar y evaluar opciones para resolver problemas rutinarios y moderadamente complejos." + }, + { + "level": 4, + "description": "Investiga la causa y el impacto, evalúa las opciones y resuelve una amplia gama de problemas complejos." + }, + { + "level": 5, + "description": "Investiga cuestiones complejas para identificar las causas fundamentales y los impactos, evalúa una variedad de soluciones y toma decisiones informadas sobre el mejor curso de acción, a menudo en colaboración con otros expertos." + }, + { + "level": 6, + "description": "Anticipa y lidera la resolución de problemas y oportunidades que puedan afectar los objetivos de la organización, estableciendo un enfoque estratégico y asignando recursos." + }, + { + "level": 7, + "description": "Gestiona las interrelaciones entre las partes afectadas y los imperativos estratégicos, reconociendo el contexto empresarial más amplio y sacando conclusiones precisas al resolver problemas." + } + ] + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability", + "levels": [ + { + "level": 1, + "description": "Acepta el cambio y está abierto a nuevas formas de trabajar." + }, + { + "level": 2, + "description": "Se ajusta a diferentes dinámicas de equipo y requisitos de trabajo.\n\nParticipa en procesos de adaptación de equipos." + }, + { + "level": 3, + "description": "Se adapta y responde al cambio y muestra iniciativa en la adopción de nuevos métodos o tecnologías." + }, + { + "level": 4, + "description": "Permite a otros adaptarse y cambiar en respuesta a los desafíos y cambios en el entorno de trabajo." + }, + { + "level": 5, + "description": "Lidera adaptaciones a entornos empresariales cambiantes.\n\nGuía a los equipos a través de las transiciones, manteniendo el enfoque en los objetivos organizacionales." + }, + { + "level": 6, + "description": "Impulsa la adaptabilidad organizacional iniciando y liderando cambios significativos. Influye en las estrategias de gestión del cambio a nivel organizacional." + }, + { + "level": 7, + "description": "Promueve la agilidad organizativa y la resiliencia.\n\nIntegra la adaptabilidad en la cultura organizacional y la planificación estratégica." + } + ] + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security", + "levels": [ + { + "level": 1, + "description": "Desarrolla una comprensión de las reglas y expectativas de su función y de la organización." + }, + { + "level": 2, + "description": "Tiene una buena comprensión de su papel y de las reglas y expectativas de la organización." + }, + { + "level": 3, + "description": "Aplica el profesionalismo adecuado y las prácticas y conocimientos prácticos para trabajar." + }, + { + "level": 4, + "description": "Adapta y aplica las normas aplicables, reconociendo su importancia en la consecución de resultados en equipo." + }, + { + "level": 5, + "description": "Contribuye de manera proactiva a la implementación de prácticas de trabajo profesionales y ayuda a promover una cultura organizacional de apoyo." + }, + { + "level": 6, + "description": "Desempeña un papel de liderazgo en la promoción y garantía de una cultura y prácticas de trabajo adecuadas, incluida la igualdad de acceso y oportunidades para las personas con capacidades diversas." + }, + { + "level": 7, + "description": "Proporciona una dirección clara y un liderazgo estratégico para integrar el cumplimiento, la cultura organizacional y las prácticas de trabajo, y promueve activamente la diversidad y la inclusión." + } + ] + } + ], + "by_level": [ + { + "level": 1, + "title": "Sigue", + "factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Trabaja principalmente en sus propias tareas y solo interactúa con su equipo inmediato. Desarrolla una comprensión de cómo su trabajo apoya a otros." + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Se comunica con el equipo inmediato para comprender y cumplir las tareas asignadas. Observa, escucha y, si se le sugiere, hace preguntas para buscar información o aclarar instrucciones." + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Identifica oportunidades de mejora en las tareas propias. Sugiere mejoras básicas cuando se le solicita." + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Participa en la generación de nuevas ideas cuando así se le solicita." + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza poca discreción en la atención de consultas.  \n\nSe espera que busque orientación en situaciones inesperadas." + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Tiene habilidades digitales básicas para aprender y utilizar aplicaciones, procesos y herramientas para su función." + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Aumenta proactivamente su comprensión de sus tareas y responsabilidades laborales." + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Aplica los conocimientos recién adquiridos para desarrollar habilidades para su rol. Contribuye a identificar las propias oportunidades de desarrollo." + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Confirma los pasos necesarios para las tareas individuales." + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Trabaja para entender el problema y busca asistencia para resolver problemas inesperados." + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Acepta el cambio y está abierto a nuevas formas de trabajar." + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Desarrolla una comprensión de las reglas y expectativas de su función y de la organización." + } + ] + }, + { + "level": 2, + "title": "Asistir", + "factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Comprende la necesidad de colaborar con su equipo y considera las necesidades del usuario/cliente." + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Comunica información familiar con el equipo inmediato y las partes interesadas directamente relacionadas con su función.\n\nEscucha para obtener comprensión y hace preguntas relevantes para aclarar o buscar más información." + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Propone ideas para mejorar el área de trabajo propia.\n\nImplementa cambios acordados en las tareas de trabajo asignadas." + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica el pensamiento creativo para sugerir nuevas formas de abordar una tarea y resolver problemas." + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza discreción limitada para resolver problemas o consultas.\n\nDecide cuándo buscar orientación en situaciones inesperadas." + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Tiene habilidades digitales suficientes para su rol; comprende y utiliza métodos, herramientas, aplicaciones y procesos apropiados." + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Se hace cargo de desarrollar su experiencia laboral." + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Absorbe y aplica nueva información a las tareas.\n\nReconoce las habilidades personales y las brechas de conocimiento y busca oportunidades de aprendizaje para abordarlas." + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Planifica su propio trabajo en cortos horizontes temporales de forma organizada." + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Investiga y resuelve problemas rutinarios." + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Se ajusta a diferentes dinámicas de equipo y requisitos de trabajo.\n\nParticipa en procesos de adaptación de equipos." + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Tiene una buena comprensión de su papel y de las reglas y expectativas de la organización." + } + ] + }, + { + "level": 3, + "title": "Aplicar", + "factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Comprende y colabora en el análisis de las necesidades del usuario/cliente y así lo representa en su trabajo." + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Se comunica con el equipo y las partes interesadas dentro y fuera de la organización, explicando y presentando información de forma clara.\n\nContribuye a una variedad de conversaciones relacionadas con el trabajo y escucha a los demás para comprender y hace preguntas de sondeo relevantes para su función." + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Identifica e implementa mejoras en su propia área de trabajo.\n\nContribuye a las mejoras de procesos a nivel de equipo." + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica y contribuye a las técnicas de pensamiento creativo para aportar nuevas ideas para su propio trabajo y para actividades en equipo." + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza discreción para identificar y responder a problemas complejos relacionados con sus propias asignaciones. \n\nDetermina cuándo los problemas deben escalarse a un nivel superior." + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Explora y aplica herramientas y habilidades digitales relevantes para su función.\n\nComprende y aplica de manera efectiva métodos, herramientas, aplicaciones y procesos adecuados." + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Proporciona orientación básica y apoyo a los miembros menos experimentados del equipo según sea necesario." + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Absorbe y aplica información nueva de manera efectiva con la capacidad de compartir aprendizajes con colegas.\n\nToma la iniciativa para identificar y negociar sus propias oportunidades de desarrollo apropiadas." + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Organiza y realiza un seguimiento de su propio trabajo (y de otros, cuando sea necesario) para cumplir los plazos acordados." + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Aplica un enfoque metódico para investigar y evaluar opciones para resolver problemas rutinarios y moderadamente complejos." + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Se adapta y responde al cambio y muestra iniciativa en la adopción de nuevos métodos o tecnologías." + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Aplica el profesionalismo adecuado y las prácticas y conocimientos prácticos para trabajar." + } + ] + }, + { + "level": 4, + "title": "Capacita", + "factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Facilita la colaboración entre partes interesadas que comparten objetivos comunes.  \n\nColabora con los equipos interfuncionales y contribuye en ellos para garantizar que se satisfagan las necesidades de los usuarios y clientes en todo el producto a entregar y el alcance de su labor." + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Se comunica tanto con audiencias técnicas como no técnicas, incluidos el equipo y las partes interesadas dentro y fuera de la organización.\n\nSegún sea necesario, toma la iniciativa en la explicación de conceptos complejos para apoyar la toma de decisiones.\n\nEscucha y hace preguntas perspicaces para identificar diferentes perspectivas a fin de aclarar y confirmar la comprensión." + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Alienta y apoya los debates en equipo sobre iniciativas de mejora.\n\nImplementa cambios de procedimiento dentro de un ámbito definido de trabajo." + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica, facilita y desarrolla conceptos de pensamiento creativo y encuentra formas alternativas de enfocar los resultados del equipo." + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza su criterio y una discreción sustancial para identificar y responder a problemas complejos y tareas relacionadas con proyectos y objetivos del equipo.\n\nEscala cuando el alcance se ve afectado." + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Maximiza las capacidades de las aplicaciones para su función, y evalúa y soporta el uso de nuevas tecnologías y herramientas digitales.\n\nSelecciona adecuadamente y evalúa el impacto de los cambios en los estándares, métodos, herramientas, aplicaciones y procesos aplicables y relevantes para la propia especialidad." + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Dirige, apoya o guía a los miembros del equipo.\n\nDesarrolla soluciones para actividades laborales complejas relacionadas con asignaciones. \n\nDemuestra una comprensión de los factores de riesgo en su trabajo.\n\nContribuye con conocimientos especializados a la definición de requisitos para respaldar propuestas." + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Absorbe rápidamente y evalúa críticamente nueva información y la aplica eficazmente.\n\nMantiene una comprensión de las prácticas emergentes y de su aplicación, y asume la responsabilidad de impulsar las oportunidades de desarrollo propias y de los miembros del equipo." + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Planifica, programa y supervisa el trabajo para cumplir determinados objetivos y procesos personales y/o de equipo, demostrando un enfoque analítico para cumplir alcanzar los objetivos de tiempo y calidad." + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Investiga la causa y el impacto, evalúa las opciones y resuelve una amplia gama de problemas complejos." + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Permite a otros adaptarse y cambiar en respuesta a los desafíos y cambios en el entorno de trabajo." + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Adapta y aplica las normas aplicables, reconociendo su importancia en la consecución de resultados en equipo." + } + ] + }, + { + "level": 5, + "title": "Asegurar, asesorar", + "factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Facilita la colaboración entre las partes interesadas que tienen diversos objetivos.\n\nGarantiza formas de trabajo colaborativas en todas las etapas del trabajo para satisfacer las necesidades del usuario/cliente.\n\nConstruye relaciones efectivas en toda la organización y con clientes, proveedores y socios." + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Comunica claramente y con impacto, articulando información e ideas complejas para audiencias amplias con diferentes puntos de vista.\n\nLidera y fomenta conversaciones para compartir ideas y generar consenso sobre las medidas a tomar." + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Identifica y evalúa mejoras potenciales de productos, prácticas o servicios.\n\nDirige la aplicación de mejoras dentro de su propia área de responsabilidades.\n\nEvalúa la efectividad de los cambios implementados." + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica de manera creativa el pensamiento innovador y prácticas de diseño para identificar soluciones que le den valor en beneficio de clientes y partes interesadas." + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza el buen juicio para tomar decisiones informadas sobre las acciones para lograr resultados organizacionales, como el cumplimiento de metas, plazos y presupuestos.\n\nPlantea cuestionamientos cuando los objetivos están en riesgo." + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Reconoce y evalúa el impacto organizacional de las nuevas tecnologías y servicios digitales.\n\nImplementa prácticas nuevas y efectivas. \n\nAsesora sobre los estándares, métodos, herramientas, aplicaciones y procesos disponibles y relevantes para especialidades grupales, y puede tomar decisiones apropiadas a partir de las alternativas." + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Proporciona liderazgo a nivel operativo.\n\nImplementa y ejecuta políticas alineadas con los planes estratégicos.\n\nEstima y evalúa riesgos.\n\nAl examinar propuestas toma en cuenta todos los requisitos." + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Utiliza sus habilidades y conocimientos para ayudar a establecer los estándares que otros en la organización aplicarán.\n\nToma la iniciativa de desarrollar una mayor amplitud de conocimientos en toda la industria y/o negocio, así como para identificar y gestionar oportunidades de desarrollo en su área de responsabilidad." + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Analiza, diseña, planifica, establece hitos y ejecuta y evalúa el trabajo según los objetivos de tiempo, costo y calidad." + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Investiga cuestiones complejas para identificar las causas fundamentales y los impactos, evalúa una variedad de soluciones y toma decisiones informadas sobre el mejor curso de acción, a menudo en colaboración con otros expertos." + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Lidera adaptaciones a entornos empresariales cambiantes.\n\nGuía a los equipos a través de las transiciones, manteniendo el enfoque en los objetivos organizacionales." + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Contribuye de manera proactiva a la implementación de prácticas de trabajo profesionales y ayuda a promover una cultura organizacional de apoyo." + } + ] + }, + { + "level": 6, + "title": "Iniciar, influir", + "factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Lidera la colaboración con una amplia gama de partes interesadas a través de objetivos contrapuestos dentro de la organización.\n\nEstablece conexiones sólidas e influyentes con contactos internos y externos fundamentales a nivel de dirección superior/líder técnico." + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Comunica con credibilidad a todos los niveles y en toda la organización a audiencias amplias con objetivos divergentes.\n\nExplica claramente información e ideas complejas, influyendo en la dirección estratégica.\n\nPromueve el intercambio de información en toda la organización." + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Impulsa iniciativas de mejora que tienen un impacto significativo en la organización.\n\nAlinea las estrategias de mejora con los objetivos organizacionales.\n\nInvolucra a las partes interesadas en los procesos de mejora." + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica creativamente una amplia gama de nuevas ideas y técnicas de gestión eficaces para lograr resultados que se alinean con la estrategia de la organización." + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza su criterio para tomar decisiones que inician el logro de los objetivos estratégicos acordados, incluido el rendimiento financiero.\n\nEscala cuando se ve afectada una dirección estratégica más amplia." + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Lidera la mejora de las capacidades digitales de la organización. \n\nIdentifica y respalda oportunidades para adoptar nuevas tecnologías y servicios digitales.\n\nLidera la gobernanza digital y el cumplimiento de la legislación pertinente, y la necesidad de productos y servicios." + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Proporciona liderazgo a nivel organizacional.\n\nContribuye al desarrollo e implementación de políticas y estrategias.\n\nEntiende y comunica la evolución de la industria, y el rol y el impacto de la tecnología. \n\nGestiona y mitiga los riesgos organizacionales.  \n\nEquilibra los requisitos de las propuestas con las necesidades más generales de la organización." + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Promueve la aplicación del conocimiento para apoyar imperativos estratégicos.\n\nDesarrolla activamente sus habilidades de liderazgo estratégico y técnico, y lidera el desarrollo de habilidades en su área de responsabilidad." + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Inicia e influye en los objetivos estratégicos y asigna responsabilidades." + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Anticipa y lidera la resolución de problemas y oportunidades que puedan afectar los objetivos de la organización, estableciendo un enfoque estratégico y asignando recursos." + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Impulsa la adaptabilidad organizacional iniciando y liderando cambios significativos. Influye en las estrategias de gestión del cambio a nivel organizacional." + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Desempeña un papel de liderazgo en la promoción y garantía de una cultura y prácticas de trabajo adecuadas, incluida la igualdad de acceso y oportunidades para las personas con capacidades diversas." + } + ] + }, + { + "level": 7, + "title": "Establecer estrategia, inspirar, movilizar", + "factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Impulsa la colaboración, comprometiéndose con las partes interesadas de liderazgo, para asegurar la alineación con la visión y la estrategia corporativas. \n\nConstruye relaciones fuertes e influyentes con clientes, socios y líderes de la industria." + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Se comunica con audiencias de todos los niveles dentro de su propia organización y se involucra con la industria.\n\nPresenta argumentos e ideas convincentes de forma autoritaria y convincente para alcanzar los objetivos de negocio." + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Define y comunica el enfoque organizacional para la mejora continua.\n\nCultiva una cultura de mejora continua.\n\nEvalúa el impacto de las iniciativas de mejora en el éxito organizacional." + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Promueve la creatividad y la innovación para impulsar el desarrollo de estrategias que permitan oportunidades comerciales." + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Aplica el buen juicio en la toma de decisiones críticas para la dirección estratégica de la organización y el éxito.\n\nEscala cuando se requiere la aportación de la dirección ejecutiva empresarial a través de estructuras de gobernanza establecidas." + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Lidera el desarrollo de la cultura digital de la organización y la visión transformadora.  \n\nPromueve la capacidad y/o la explotación de la tecnología dentro de una o más organizaciones a través de una comprensión profunda de la industria y las implicaciones de las tecnologías emergentes.\n\nEs responsable de evaluar cómo las leyes y reglamentaciones impactan los objetivos organizacionales y el uso de las capacidades digitales, de datos y tecnología." + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Lidera la gestión estratégica.\n\nAplica el más alto nivel de liderazgo a la formulación e implementación de la estrategia.\n\nComunica el impacto potencial de prácticas y tecnologías emergentes en organizaciones e individuos y evalúa los riesgos de usar o no tales prácticas y tecnologías. \n\nEstablece la gobernanza para abordar los riesgos empresariales.\n\nAsegura que las propuestas se alineen con la dirección estratégica de la organización." + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Inspira una cultura de aprendizaje para alinearse con los objetivos de negocio.   \n\nMantiene una visión estratégica de los panoramas contemporáneos y emergentes de la industria. \n\nSe asegura de que la organización desarrolle y movilice toda la gama de capacidades y habilidades requeridas." + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Planifica y lidera al más alto nivel de autoridad sobre todos los aspectos de un área de trabajo considerable." + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Gestiona las interrelaciones entre las partes afectadas y los imperativos estratégicos, reconociendo el contexto empresarial más amplio y sacando conclusiones precisas al resolver problemas." + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Promueve la agilidad organizativa y la resiliencia.\n\nIntegra la adaptabilidad en la cultura organizacional y la planificación estratégica." + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Proporciona una dirección clara y un liderazgo estratégico para integrar el cumplimiento, la cultura organizacional y las prácticas de trabajo, y promueve activamente la diversidad y la inclusión." + } + ] + } + ], + "source": { + "generic_attributes_url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours", + "behavioural_factors_pdf": "https://sfia-online.org/en/sfia-9/responsibilities/sfia-9-alternative-presentation-of-behavioural-factors.pdf" + } +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/es/index.json b/projects/odilo/data/sfia-9-json/es/index.json new file mode 100644 index 000000000..c222c2fde --- /dev/null +++ b/projects/odilo/data/sfia-9-json/es/index.json @@ -0,0 +1,1052 @@ +{ + "type": "sfia.index", + "sfia_version": 9, + "language": "es", + "generated_at": "2026-02-02", + "source_file": "sfia_source/SFIA 9 Excel - Español/sfia-9_current-standard_es_250104.xlsx", + "source_date": null, + "counts": { + "skills": 147, + "attributes": 16, + "levels_of_responsibility": 7 + }, + "paths": { + "skills_dir": "skills/", + "attributes": "attributes.json", + "levels_of_responsibility": "levels-of-responsibility.json", + "terms_of_use": "terms-of-use.json", + "responsibilities": "responsibilities.json", + "behaviour_matrix": "behaviour-matrix.json" + }, + "skills": [ + { + "code": "ITSP", + "name": "Planificación estratégica", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/ITSP.json" + }, + { + "code": "ISCO", + "name": "Coordinación de sistemas de información", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/ISCO.json" + }, + { + "code": "IRMG", + "name": "Gestión de la información", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/IRMG.json" + }, + { + "code": "STPL", + "name": "Arquitectura empresarial y de negocios", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/STPL.json" + }, + { + "code": "ARCH", + "name": "Arquitectura de solución", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/ARCH.json" + }, + { + "code": "INOV", + "name": "Gestión de la innovación", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/INOV.json" + }, + { + "code": "EMRG", + "name": "Monitoreo de la tecnología emergente", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/EMRG.json" + }, + { + "code": "RSCH", + "name": "Investigación formal", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/RSCH.json" + }, + { + "code": "SUST", + "name": "Sostenibilidad", + "category": "Estrategia y arquitectura", + "subcategory": "Estrategia y planificación", + "file": "skills/SUST.json" + }, + { + "code": "FMIT", + "name": "Gestión financiera", + "category": "Estrategia y arquitectura", + "subcategory": "Gestión financiera y de valor", + "file": "skills/FMIT.json" + }, + { + "code": "INVA", + "name": "Evaluación de inversiones", + "category": "Estrategia y arquitectura", + "subcategory": "Gestión financiera y de valor", + "file": "skills/INVA.json" + }, + { + "code": "BENM", + "name": "Gestión de beneficios", + "category": "Estrategia y arquitectura", + "subcategory": "Gestión financiera y de valor", + "file": "skills/BENM.json" + }, + { + "code": "BUDF", + "name": "Presupuesto y previsión", + "category": "Estrategia y arquitectura", + "subcategory": "Gestión financiera y de valor", + "file": "skills/BUDF.json" + }, + { + "code": "FIAN", + "name": "Análisis financiero", + "category": "Estrategia y arquitectura", + "subcategory": "Gestión financiera y de valor", + "file": "skills/FIAN.json" + }, + { + "code": "COMG", + "name": "Gestión de costos", + "category": "Estrategia y arquitectura", + "subcategory": "Gestión financiera y de valor", + "file": "skills/COMG.json" + }, + { + "code": "DEMM", + "name": "Gestión de la demanda", + "category": "Estrategia y arquitectura", + "subcategory": "Gestión financiera y de valor", + "file": "skills/DEMM.json" + }, + { + "code": "MEAS", + "name": "Medición", + "category": "Estrategia y arquitectura", + "subcategory": "Gestión financiera y de valor", + "file": "skills/MEAS.json" + }, + { + "code": "SCTY", + "name": "Seguridad de la información", + "category": "Estrategia y arquitectura", + "subcategory": "Seguridad y privacidad", + "file": "skills/SCTY.json" + }, + { + "code": "INAS", + "name": "Aseguramiento de la información", + "category": "Estrategia y arquitectura", + "subcategory": "Seguridad y privacidad", + "file": "skills/INAS.json" + }, + { + "code": "THIN", + "name": "Inteligencia de amenazas", + "category": "Estrategia y arquitectura", + "subcategory": "Seguridad y privacidad", + "file": "skills/THIN.json" + }, + { + "code": "GOVN", + "name": "Gobernanza", + "category": "Estrategia y arquitectura", + "subcategory": "Gobierno, riesgo y cumplimiento", + "file": "skills/GOVN.json" + }, + { + "code": "BURM", + "name": "Gestión del riesgo", + "category": "Estrategia y arquitectura", + "subcategory": "Gobierno, riesgo y cumplimiento", + "file": "skills/BURM.json" + }, + { + "code": "AIDE", + "name": "Inteligencia artificial (IA) y ética de datos", + "category": "Estrategia y arquitectura", + "subcategory": "Gobierno, riesgo y cumplimiento", + "file": "skills/AIDE.json" + }, + { + "code": "AUDT", + "name": "Auditar", + "category": "Estrategia y arquitectura", + "subcategory": "Gobierno, riesgo y cumplimiento", + "file": "skills/AUDT.json" + }, + { + "code": "QUMG", + "name": "Gestión de la calidad", + "category": "Estrategia y arquitectura", + "subcategory": "Gobierno, riesgo y cumplimiento", + "file": "skills/QUMG.json" + }, + { + "code": "QUAS", + "name": "Aseguramiento de la calidad", + "category": "Estrategia y arquitectura", + "subcategory": "Gobierno, riesgo y cumplimiento", + "file": "skills/QUAS.json" + }, + { + "code": "CNSL", + "name": "Consultoría", + "category": "Estrategia y arquitectura", + "subcategory": "Asesoramiento y orientación", + "file": "skills/CNSL.json" + }, + { + "code": "TECH", + "name": "Asesoramiento especializado", + "category": "Estrategia y arquitectura", + "subcategory": "Asesoramiento y orientación", + "file": "skills/TECH.json" + }, + { + "code": "PEDP", + "name": "Información y cumplimiento de datos", + "category": "Estrategia y arquitectura", + "subcategory": "Seguridad y privacidad", + "file": "skills/PEDP.json" + }, + { + "code": "METL", + "name": "Métodos y herramientas", + "category": "Estrategia y arquitectura", + "subcategory": "Asesoramiento y orientación", + "file": "skills/METL.json" + }, + { + "code": "POMG", + "name": "Dirección de la cartera de proyectos", + "category": "Cambio y transformación", + "subcategory": "Implementación del cambio empresarial", + "file": "skills/POMG.json" + }, + { + "code": "PGMG", + "name": "Dirección de programas", + "category": "Cambio y transformación", + "subcategory": "Implementación del cambio empresarial", + "file": "skills/PGMG.json" + }, + { + "code": "PRMG", + "name": "Dirección de proyectos", + "category": "Cambio y transformación", + "subcategory": "Implementación del cambio empresarial", + "file": "skills/PRMG.json" + }, + { + "code": "PROF", + "name": "Apoyo de cartera, programa y proyecto", + "category": "Cambio y transformación", + "subcategory": "Implementación del cambio empresarial", + "file": "skills/PROF.json" + }, + { + "code": "DEMG", + "name": "Gestión de la entrega", + "category": "Cambio y transformación", + "subcategory": "Implementación del cambio empresarial", + "file": "skills/DEMG.json" + }, + { + "code": "BUSA", + "name": "Análisis de la situación empresarial", + "category": "Cambio y transformación", + "subcategory": "Análisis de cambios", + "file": "skills/BUSA.json" + }, + { + "code": "FEAS", + "name": "Evaluación de viabilidad", + "category": "Cambio y transformación", + "subcategory": "Análisis de cambios", + "file": "skills/FEAS.json" + }, + { + "code": "REQM", + "name": "Gestión y definición de requerimientos", + "category": "Cambio y transformación", + "subcategory": "Análisis de cambios", + "file": "skills/REQM.json" + }, + { + "code": "BSMO", + "name": "Modelado de negocios", + "category": "Cambio y transformación", + "subcategory": "Análisis de cambios", + "file": "skills/BSMO.json" + }, + { + "code": "BPTS", + "name": "Pruebas de aceptación del usuario final", + "category": "Cambio y transformación", + "subcategory": "Análisis de cambios", + "file": "skills/BPTS.json" + }, + { + "code": "VURE", + "name": "Investigación de vulnerabilidades", + "category": "Estrategia y arquitectura", + "subcategory": "Seguridad y privacidad", + "file": "skills/VURE.json" + }, + { + "code": "BPRE", + "name": "Mejora de procesos de negocio", + "category": "Cambio y transformación", + "subcategory": "Planificación de cambios", + "file": "skills/BPRE.json" + }, + { + "code": "OCEN", + "name": "Habilitación del cambio organizacional", + "category": "Cambio y transformación", + "subcategory": "Planificación de cambios", + "file": "skills/OCEN.json" + }, + { + "code": "OCDV", + "name": "Desarrollo de las capacidades organizativas", + "category": "Cambio y transformación", + "subcategory": "Planificación de cambios", + "file": "skills/OCDV.json" + }, + { + "code": "ORDI", + "name": "Implementación y diseño de la organización", + "category": "Cambio y transformación", + "subcategory": "Planificación de cambios", + "file": "skills/ORDI.json" + }, + { + "code": "JADN", + "name": "Análisis y diseño de puestos", + "category": "Cambio y transformación", + "subcategory": "Planificación de cambios", + "file": "skills/JADN.json" + }, + { + "code": "CIPM", + "name": "Gestión del cambio organizativo", + "category": "Cambio y transformación", + "subcategory": "Planificación de cambios", + "file": "skills/CIPM.json" + }, + { + "code": "PROD", + "name": "Gestión de productos", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/PROD.json" + }, + { + "code": "DLMG", + "name": "Gestión de desarrollo de sistemas", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/DLMG.json" + }, + { + "code": "SLEN", + "name": "Ingeniería del ciclo de vida de sistemas y software", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/SLEN.json" + }, + { + "code": "DESN", + "name": "Diseño de sistemas", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/DESN.json" + }, + { + "code": "SWDN", + "name": "Diseño de software", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/SWDN.json" + }, + { + "code": "NTDS", + "name": "Diseño de redes", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/NTDS.json" + }, + { + "code": "IFDN", + "name": "Diseño de infraestructura", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/IFDN.json" + }, + { + "code": "HWDE", + "name": "Diseño de hardware", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/HWDE.json" + }, + { + "code": "PROG", + "name": "Programación/Desarrollo de software", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/PROG.json" + }, + { + "code": "SINT", + "name": "Construcción e integración de sistemas", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/SINT.json" + }, + { + "code": "TEST", + "name": "Pruebas funcionales", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/TEST.json" + }, + { + "code": "NFTS", + "name": "Pruebas de aspectos no funcionales", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/NFTS.json" + }, + { + "code": "PRTS", + "name": "Pruebas del proceso", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/PRTS.json" + }, + { + "code": "PORT", + "name": "Configuración de producto de software", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/PORT.json" + }, + { + "code": "RESD", + "name": "Desarrollo de sistemas de tiempo real/embebidos", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/RESD.json" + }, + { + "code": "SFEN", + "name": "Ingeniería de seguridad", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/SFEN.json" + }, + { + "code": "SFAS", + "name": "Evaluación de la seguridad", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/SFAS.json" + }, + { + "code": "RFEN", + "name": "Ingeniería de radiofrecuencia", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/RFEN.json" + }, + { + "code": "ADEV", + "name": "Desarrollo de animación", + "category": "Desarrollo e implementación", + "subcategory": "Desarrollo de sistemas", + "file": "skills/ADEV.json" + }, + { + "code": "DATM", + "name": "Gestión de datos", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/DATM.json" + }, + { + "code": "DTAN", + "name": "Modelado y diseño de datos", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/DTAN.json" + }, + { + "code": "DBDS", + "name": "Diseño de base de datos", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/DBDS.json" + }, + { + "code": "DAAN", + "name": "Analítica de datos", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/DAAN.json" + }, + { + "code": "DATS", + "name": "Ciencia de datos", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/DATS.json" + }, + { + "code": "MLNG", + "name": "Aprendizaje automático", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/MLNG.json" + }, + { + "code": "BINT", + "name": "Inteligencia empresarial", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/BINT.json" + }, + { + "code": "DENG", + "name": "Ingeniería de datos", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/DENG.json" + }, + { + "code": "VISL", + "name": "Visualización de datos", + "category": "Desarrollo e implementación", + "subcategory": "Datos y analítica", + "file": "skills/VISL.json" + }, + { + "code": "URCH", + "name": "Investigación de usuarios", + "category": "Desarrollo e implementación", + "subcategory": "Diseño centrado en el usuario", + "file": "skills/URCH.json" + }, + { + "code": "CEXP", + "name": "Experiencia del cliente", + "category": "Desarrollo e implementación", + "subcategory": "Diseño centrado en el usuario", + "file": "skills/CEXP.json" + }, + { + "code": "ACIN", + "name": "Accesibilidad e inclusión", + "category": "Desarrollo e implementación", + "subcategory": "Diseño centrado en el usuario", + "file": "skills/ACIN.json" + }, + { + "code": "UNAN", + "name": "Análisis de experiencia de usuario", + "category": "Desarrollo e implementación", + "subcategory": "Diseño centrado en el usuario", + "file": "skills/UNAN.json" + }, + { + "code": "HCEV", + "name": "Diseño de experiencia de usuario", + "category": "Desarrollo e implementación", + "subcategory": "Diseño centrado en el usuario", + "file": "skills/HCEV.json" + }, + { + "code": "USEV", + "name": "Evaluación de experiencia de usuario", + "category": "Desarrollo e implementación", + "subcategory": "Diseño centrado en el usuario", + "file": "skills/USEV.json" + }, + { + "code": "INCA", + "name": "Diseño y autoría de contenidos", + "category": "Desarrollo e implementación", + "subcategory": "Gestión de contenido", + "file": "skills/INCA.json" + }, + { + "code": "ICPM", + "name": "Publicación de contenido", + "category": "Desarrollo e implementación", + "subcategory": "Gestión de contenido", + "file": "skills/ICPM.json" + }, + { + "code": "KNOW", + "name": "Gestión del conocimiento", + "category": "Desarrollo e implementación", + "subcategory": "Gestión de contenido", + "file": "skills/KNOW.json" + }, + { + "code": "GRDN", + "name": "Diseño gráfico", + "category": "Desarrollo e implementación", + "subcategory": "Gestión de contenido", + "file": "skills/GRDN.json" + }, + { + "code": "SCMO", + "name": "Modelado científico", + "category": "Desarrollo e implementación", + "subcategory": "Ciencia computacional", + "file": "skills/SCMO.json" + }, + { + "code": "NUAN", + "name": "Análisis numérico", + "category": "Desarrollo e implementación", + "subcategory": "Ciencia computacional", + "file": "skills/NUAN.json" + }, + { + "code": "HPCC", + "name": "Computación de alto rendimiento", + "category": "Desarrollo e implementación", + "subcategory": "Ciencia computacional", + "file": "skills/HPCC.json" + }, + { + "code": "ITMG", + "name": "Gestión de servicios tecnológicos", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/ITMG.json" + }, + { + "code": "ASUP", + "name": "Soporte de aplicaciones", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/ASUP.json" + }, + { + "code": "ITOP", + "name": "Operaciones de infraestructura", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/ITOP.json" + }, + { + "code": "SYSP", + "name": "Administración del software de sistema", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/SYSP.json" + }, + { + "code": "NTAS", + "name": "Soporte de red", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/NTAS.json" + }, + { + "code": "HSIN", + "name": "Instalación y desinstalación de sistemas", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/HSIN.json" + }, + { + "code": "CFMG", + "name": "Gestión de la configuración", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/CFMG.json" + }, + { + "code": "RELM", + "name": "Gestión de liberaciones", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/RELM.json" + }, + { + "code": "STMG", + "name": "Gestión de almacenamiento", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/STMG.json" + }, + { + "code": "DCMA", + "name": "Gestión de instalaciones", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/DCMA.json" + }, + { + "code": "SLMO", + "name": "Gestión de nivel de servicio", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/SLMO.json" + }, + { + "code": "SCMG", + "name": "Gestión del catálogo de servicios", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/SCMG.json" + }, + { + "code": "AVMT", + "name": "Gestión de la disponibilidad", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/AVMT.json" + }, + { + "code": "COPL", + "name": "Gestión de la continuidad", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/COPL.json" + }, + { + "code": "CPMG", + "name": "Gestión de la capacidad", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/CPMG.json" + }, + { + "code": "USUP", + "name": "Gestión de incidentes", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/USUP.json" + }, + { + "code": "PBMG", + "name": "Gestión de problemas", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/PBMG.json" + }, + { + "code": "CHMG", + "name": "Control de cambios", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/CHMG.json" + }, + { + "code": "ASMG", + "name": "Gestión de activos", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/ASMG.json" + }, + { + "code": "SEAC", + "name": "Aceptación del servicio", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de servicios", + "file": "skills/SEAC.json" + }, + { + "code": "IAMT", + "name": "Gestión de identidad y acceso", + "category": "Prestación y funcionamiento", + "subcategory": "Servicios de seguridad", + "file": "skills/IAMT.json" + }, + { + "code": "SCAD", + "name": "Operaciones de seguridad", + "category": "Prestación y funcionamiento", + "subcategory": "Servicios de seguridad", + "file": "skills/SCAD.json" + }, + { + "code": "VUAS", + "name": "Evaluación de vulnerabilidades", + "category": "Prestación y funcionamiento", + "subcategory": "Servicios de seguridad", + "file": "skills/VUAS.json" + }, + { + "code": "DGFS", + "name": "Análisis forense digital", + "category": "Prestación y funcionamiento", + "subcategory": "Servicios de seguridad", + "file": "skills/DGFS.json" + }, + { + "code": "CRIM", + "name": "Investigación de delitos cibernéticos", + "category": "Prestación y funcionamiento", + "subcategory": "Servicios de seguridad", + "file": "skills/CRIM.json" + }, + { + "code": "OCOP", + "name": "Operaciones cibernéticas ofensivas", + "category": "Prestación y funcionamiento", + "subcategory": "Servicios de seguridad", + "file": "skills/OCOP.json" + }, + { + "code": "PENT", + "name": "Pruebas de penetración", + "category": "Prestación y funcionamiento", + "subcategory": "Servicios de seguridad", + "file": "skills/PENT.json" + }, + { + "code": "RMGT", + "name": "Gestión de registros", + "category": "Prestación y funcionamiento", + "subcategory": "Operaciones de datos y registros", + "file": "skills/RMGT.json" + }, + { + "code": "ANCC", + "name": "Clasificación analítica y codificación", + "category": "Prestación y funcionamiento", + "subcategory": "Operaciones de datos y registros", + "file": "skills/ANCC.json" + }, + { + "code": "DBAD", + "name": "Administración de bases de datos", + "category": "Prestación y funcionamiento", + "subcategory": "Operaciones de datos y registros", + "file": "skills/DBAD.json" + }, + { + "code": "PEMT", + "name": "Gestión del desempeño", + "category": "Personas y habilidades", + "subcategory": "Gestión de personas", + "file": "skills/PEMT.json" + }, + { + "code": "EEXP", + "name": "Experiencia de los empleados", + "category": "Personas y habilidades", + "subcategory": "Gestión de personas", + "file": "skills/EEXP.json" + }, + { + "code": "OFCL", + "name": "Facilitación organizativa", + "category": "Personas y habilidades", + "subcategory": "Gestión de personas", + "file": "skills/OFCL.json" + }, + { + "code": "DEPL", + "name": "Despliegue", + "category": "Prestación y funcionamiento", + "subcategory": "Gestión de tecnologías", + "file": "skills/DEPL.json" + }, + { + "code": "PDSV", + "name": "Desarrollo profesional", + "category": "Personas y habilidades", + "subcategory": "Gestión de personas", + "file": "skills/PDSV.json" + }, + { + "code": "WFPL", + "name": "Planificación de la fuerza laboral", + "category": "Personas y habilidades", + "subcategory": "Gestión de personas", + "file": "skills/WFPL.json" + }, + { + "code": "RESC", + "name": "Administración de recursos", + "category": "Personas y habilidades", + "subcategory": "Gestión de personas", + "file": "skills/RESC.json" + }, + { + "code": "ETMG", + "name": "Gestión de aprendizaje y desarrollo", + "category": "Personas y habilidades", + "subcategory": "Gestión de habilidades", + "file": "skills/ETMG.json" + }, + { + "code": "TMCR", + "name": "Desarrollo y diseño de aprendizaje", + "category": "Personas y habilidades", + "subcategory": "Gestión de habilidades", + "file": "skills/TMCR.json" + }, + { + "code": "ETDL", + "name": "Entrega de aprendizaje", + "category": "Personas y habilidades", + "subcategory": "Gestión de habilidades", + "file": "skills/ETDL.json" + }, + { + "code": "LEDA", + "name": "Evaluación de competencias", + "category": "Personas y habilidades", + "subcategory": "Gestión de habilidades", + "file": "skills/LEDA.json" + }, + { + "code": "CSOP", + "name": "Operación del esquema de certificación", + "category": "Personas y habilidades", + "subcategory": "Gestión de habilidades", + "file": "skills/CSOP.json" + }, + { + "code": "TEAC", + "name": "Enseñanza", + "category": "Personas y habilidades", + "subcategory": "Gestión de habilidades", + "file": "skills/TEAC.json" + }, + { + "code": "SUBF", + "name": "Formación de materias", + "category": "Personas y habilidades", + "subcategory": "Gestión de habilidades", + "file": "skills/SUBF.json" + }, + { + "code": "SORC", + "name": "Adquisición", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de los interesados", + "file": "skills/SORC.json" + }, + { + "code": "SUPP", + "name": "Gestión de proveedores", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de los interesados", + "file": "skills/SUPP.json" + }, + { + "code": "ITCM", + "name": "Gestión de contratos", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de los interesados", + "file": "skills/ITCM.json" + }, + { + "code": "RLMT", + "name": "Gestión de las relaciones con las partes interesadas", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de los interesados", + "file": "skills/RLMT.json" + }, + { + "code": "CSMG", + "name": "Soporte de servicio al cliente", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de los interesados", + "file": "skills/CSMG.json" + }, + { + "code": "ADMN", + "name": "Administración de empresas", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de los interesados", + "file": "skills/ADMN.json" + }, + { + "code": "BIDM", + "name": "Gestión de ofertas/propuestas", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de ventas y ofertas", + "file": "skills/BIDM.json" + }, + { + "code": "SALE", + "name": "Venta", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de ventas y ofertas", + "file": "skills/SALE.json" + }, + { + "code": "SSUP", + "name": "Soporte de ventas", + "category": "Relaciones y compromiso", + "subcategory": "Gestión de ventas y ofertas", + "file": "skills/SSUP.json" + }, + { + "code": "MKTG", + "name": "Gestión de mercadeo", + "category": "Relaciones y compromiso", + "subcategory": "Marketing", + "file": "skills/MKTG.json" + }, + { + "code": "MRCH", + "name": "Estudios de mercado", + "category": "Relaciones y compromiso", + "subcategory": "Marketing", + "file": "skills/MRCH.json" + }, + { + "code": "BRMG", + "name": "Gestión de marca", + "category": "Relaciones y compromiso", + "subcategory": "Marketing", + "file": "skills/BRMG.json" + }, + { + "code": "MKCM", + "name": "Gestión de campañas de mercadeo", + "category": "Relaciones y compromiso", + "subcategory": "Marketing", + "file": "skills/MKCM.json" + }, + { + "code": "CELO", + "name": "Compromiso y lealtad de los clientes", + "category": "Relaciones y compromiso", + "subcategory": "Marketing", + "file": "skills/CELO.json" + }, + { + "code": "DIGM", + "name": "Mercadeo digital", + "category": "Relaciones y compromiso", + "subcategory": "Marketing", + "file": "skills/DIGM.json" + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/es/levels-of-responsibility.json b/projects/odilo/data/sfia-9-json/es/levels-of-responsibility.json new file mode 100644 index 000000000..a9d544b41 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/es/levels-of-responsibility.json @@ -0,0 +1,49 @@ +{ + "type": "sfia.levels_of_responsibility", + "sfia_version": 9, + "language": "es", + "items": [ + { + "level": 1, + "guiding_phrase": "Sigue", + "essence": "Esencia del nivel: Realiza tareas rutinarias bajo estrecha supervisión, sigue instrucciones, y requiere orientación para completar su trabajo. Aprende y aplica habilidades y conocimientos básicos.", + "url": "https://sfia-online.org/es/lor/9/1" + }, + { + "level": 2, + "guiding_phrase": "Asistir", + "essence": "Esencia del nivel: Proporciona asistencia a otros, trabaja bajo supervisión de rutina y usa su discreción para abordar problemas rutinarios. Aprende activamente a través de entrenamiento y experiencias en el trabajo.", + "url": "https://sfia-online.org/es/lor/9/2" + }, + { + "level": 3, + "guiding_phrase": "Aplicar", + "essence": "Esencia del nivel: Realiza tareas variadas, a veces complejas y no rutinarias, utilizando métodos y procedimientos estándar. Trabaja bajo dirección general, ejerce discreción y gestiona el propio trabajo dentro de los plazos. Potencia proactivamente las habilidades y el impacto en el lugar de trabajo.", + "url": "https://sfia-online.org/es/lor/9/3" + }, + { + "level": 4, + "guiding_phrase": "Capacita", + "essence": "Esencia del nivel: Realiza diversas actividades complejas, apoya y guía a otros, delega tareas cuando corresponde, trabaja de forma autónoma bajo dirección general y aporta experiencia para cumplir los objetivos del equipo.", + "url": "https://sfia-online.org/es/lor/9/4" + }, + { + "level": 5, + "guiding_phrase": "Asegurar, asesorar", + "essence": "Esencia del nivel: Proporciona orientación autorizada en su campo y trabaja bajo una dirección amplia. Responsable de entregar resultados de trabajo significativos, desde el análisis hasta la ejecución y evaluación.", + "url": "https://sfia-online.org/es/lor/9/5" + }, + { + "level": 6, + "guiding_phrase": "Iniciar, influir", + "essence": "Esencia del nivel: Tiene una influencia organizativa considerable, toma decisiones de alto nivel, moldea políticas, demuestra liderazgo, promueve la colaboración organizacional y acepta la rendición de cuentas en áreas clave.", + "url": "https://sfia-online.org/es/lor/9/6" + }, + { + "level": 7, + "guiding_phrase": "Establecer estrategia, inspirar, movilizar", + "essence": "Esencia del nivel: Opera al más alto nivel organizacional, determina la visión y la estrategia general de la organización y asume la responsabilidad por el éxito general.", + "url": "https://sfia-online.org/es/lor/9/7" + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/es/responsibilities.json b/projects/odilo/data/sfia-9-json/es/responsibilities.json new file mode 100644 index 000000000..614f44e46 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/es/responsibilities.json @@ -0,0 +1,762 @@ +{ + "type": "sfia.responsibilities", + "sfia_version": 9, + "language": "es", + "guidance_notes": "Los niveles de SFIA representan niveles de responsabilidad en el lugar de trabajo. Cada nivel sucesivo describe un mayor impacto, responsabilidad y rendicion de cuentas.\n- La autonomia, la influencia y la complejidad son atributos genericos que indican el nivel de responsabilidad.\n- Las habilidades de negocio y los factores de comportamiento describen los comportamientos requeridos para ser efectivo en cada nivel.\n- El atributo de conocimiento define la profundidad y amplitud de comprension requerida para realizar e influir en el trabajo de manera efectiva.\nComprender estos atributos le ayudara a aprovechar al maximo SFIA. Son fundamentales para comprender y aplicar los niveles descritos en las descripciones de habilidades de SFIA.", + "levels": [ + { + "level": 1, + "title": "Sigue", + "guiding_phrase": "Sigue", + "essence": "Esencia del nivel: Realiza tareas rutinarias bajo estrecha supervisión, sigue instrucciones, y requiere orientación para completar su trabajo. Aprende y aplica habilidades y conocimientos básicos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-1", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomía", + "description": "Sigue instrucciones y trabaja bajo una dirección cercana. Recibe instrucciones y orientaciones específicas, hace revisar de cerca el trabajo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influencia", + "description": "Cuando es necesario, contribuye a las discusiones del equipo con sus colegas inmediatos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complejidad", + "description": "Realiza actividades rutinarias en un ambiente estructurado.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conocimiento", + "description": "Aplica los conocimientos básicos para realizar tareas rutinarias, bien definidas y predecibles específicas del rol.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Trabaja principalmente en sus propias tareas y solo interactúa con su equipo inmediato. Desarrolla una comprensión de cómo su trabajo apoya a otros.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Se comunica con el equipo inmediato para comprender y cumplir las tareas asignadas. Observa, escucha y, si se le sugiere, hace preguntas para buscar información o aclarar instrucciones.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Identifica oportunidades de mejora en las tareas propias. Sugiere mejoras básicas cuando se le solicita.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Participa en la generación de nuevas ideas cuando así se le solicita.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza poca discreción en la atención de consultas.  \n\nSe espera que busque orientación en situaciones inesperadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Tiene habilidades digitales básicas para aprender y utilizar aplicaciones, procesos y herramientas para su función.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Aumenta proactivamente su comprensión de sus tareas y responsabilidades laborales.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Aplica los conocimientos recién adquiridos para desarrollar habilidades para su rol. Contribuye a identificar las propias oportunidades de desarrollo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Confirma los pasos necesarios para las tareas individuales.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Trabaja para entender el problema y busca asistencia para resolver problemas inesperados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Acepta el cambio y está abierto a nuevas formas de trabajar.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Desarrolla una comprensión de las reglas y expectativas de su función y de la organización.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 2, + "title": "Asistir", + "guiding_phrase": "Asistir", + "essence": "Esencia del nivel: Proporciona asistencia a otros, trabaja bajo supervisión de rutina y usa su discreción para abordar problemas rutinarios. Aprende activamente a través de entrenamiento y experiencias en el trabajo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-2", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomía", + "description": "Trabaja bajo dirección rutinaria. Recibe instrucciones y orientación, su trabajo es revisado regularmente.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influencia", + "description": "Se espera que contribuya a las discusiones del equipo con los miembros inmediatos del equipo. Trabaja junto a los miembros del equipo, contribuyendo a las decisiones del equipo. Cuando el rol lo requiere, interactúa con personas fuera de su equipo, incluyendo colegas internos y contactos externos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complejidad", + "description": "Realiza una variedad de actividades laborales en diversos entornos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conocimiento", + "description": "Aplica el conocimiento de las tareas y prácticas comunes del lugar de trabajo para apoyar las actividades del equipo bajo orientación.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Comprende la necesidad de colaborar con su equipo y considera las necesidades del usuario/cliente.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Comunica información familiar con el equipo inmediato y las partes interesadas directamente relacionadas con su función.\n\nEscucha para obtener comprensión y hace preguntas relevantes para aclarar o buscar más información.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Propone ideas para mejorar el área de trabajo propia.\n\nImplementa cambios acordados en las tareas de trabajo asignadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica el pensamiento creativo para sugerir nuevas formas de abordar una tarea y resolver problemas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza discreción limitada para resolver problemas o consultas.\n\nDecide cuándo buscar orientación en situaciones inesperadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Tiene habilidades digitales suficientes para su rol; comprende y utiliza métodos, herramientas, aplicaciones y procesos apropiados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Se hace cargo de desarrollar su experiencia laboral.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Absorbe y aplica nueva información a las tareas.\n\nReconoce las habilidades personales y las brechas de conocimiento y busca oportunidades de aprendizaje para abordarlas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Planifica su propio trabajo en cortos horizontes temporales de forma organizada.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Investiga y resuelve problemas rutinarios.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Se ajusta a diferentes dinámicas de equipo y requisitos de trabajo.\n\nParticipa en procesos de adaptación de equipos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Tiene una buena comprensión de su papel y de las reglas y expectativas de la organización.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 3, + "title": "Aplicar", + "guiding_phrase": "Aplicar", + "essence": "Esencia del nivel: Realiza tareas variadas, a veces complejas y no rutinarias, utilizando métodos y procedimientos estándar. Trabaja bajo dirección general, ejerce discreción y gestiona el propio trabajo dentro de los plazos. Potencia proactivamente las habilidades y el impacto en el lugar de trabajo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-3", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomía", + "description": "Trabaja bajo dirección general para completar las tareas asignadas. Recibe orientación y hace revisar el trabajo en hitos acordados. Cuando es necesario, delega tareas rutinarias a otros dentro de su propio equipo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influencia", + "description": "Trabaja e influye en las decisiones del equipo. Tiene un nivel transaccional de contacto con personas ajenas a su equipo, incluso colegas internos y contactos externos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complejidad", + "description": "Realiza una variedad de trabajos, a veces complejos y no rutinarios, en ambientes variados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conocimiento", + "description": "Aplica el conocimiento de una serie de prácticas específicas de roles para completar tareas dentro de límites definidos y tiene una apreciación de cómo este conocimiento se aplica al contexto empresarial más amplio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Comprende y colabora en el análisis de las necesidades del usuario/cliente y así lo representa en su trabajo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Se comunica con el equipo y las partes interesadas dentro y fuera de la organización, explicando y presentando información de forma clara.\n\nContribuye a una variedad de conversaciones relacionadas con el trabajo y escucha a los demás para comprender y hace preguntas de sondeo relevantes para su función.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Identifica e implementa mejoras en su propia área de trabajo.\n\nContribuye a las mejoras de procesos a nivel de equipo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica y contribuye a las técnicas de pensamiento creativo para aportar nuevas ideas para su propio trabajo y para actividades en equipo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza discreción para identificar y responder a problemas complejos relacionados con sus propias asignaciones. \n\nDetermina cuándo los problemas deben escalarse a un nivel superior.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Explora y aplica herramientas y habilidades digitales relevantes para su función.\n\nComprende y aplica de manera efectiva métodos, herramientas, aplicaciones y procesos adecuados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Proporciona orientación básica y apoyo a los miembros menos experimentados del equipo según sea necesario.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Absorbe y aplica información nueva de manera efectiva con la capacidad de compartir aprendizajes con colegas.\n\nToma la iniciativa para identificar y negociar sus propias oportunidades de desarrollo apropiadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Organiza y realiza un seguimiento de su propio trabajo (y de otros, cuando sea necesario) para cumplir los plazos acordados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Aplica un enfoque metódico para investigar y evaluar opciones para resolver problemas rutinarios y moderadamente complejos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Se adapta y responde al cambio y muestra iniciativa en la adopción de nuevos métodos o tecnologías.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Aplica el profesionalismo adecuado y las prácticas y conocimientos prácticos para trabajar.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 4, + "title": "Capacita", + "guiding_phrase": "Capacita", + "essence": "Esencia del nivel: Realiza diversas actividades complejas, apoya y guía a otros, delega tareas cuando corresponde, trabaja de forma autónoma bajo dirección general y aporta experiencia para cumplir los objetivos del equipo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-4", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomía", + "description": "Trabaja bajo dirección general dentro de un claro marco de rendición de cuentas. Ejerce una considerable responsabilidad personal y autonomía. Cuando es necesario, planifica, programa y delega el trabajo en otros, normalmente dentro del propio equipo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influencia", + "description": "Influye en los proyectos y en los objetivos del equipo. Tiene un nivel táctico de contacto con personas ajenas a su equipo, incluidos colegas internos y contactos externos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complejidad", + "description": "El trabajo incluye una amplia gama de actividades técnicas o profesionales complejas en diversos contextos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conocimiento", + "description": "Aplica el conocimiento en diferentes áreas en su campo, integrando este conocimiento para realizar tareas complejas y diversas. Aplica un conocimiento práctico del dominio de la organización.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Facilita la colaboración entre partes interesadas que comparten objetivos comunes.  \n\nColabora con los equipos interfuncionales y contribuye en ellos para garantizar que se satisfagan las necesidades de los usuarios y clientes en todo el producto a entregar y el alcance de su labor.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Se comunica tanto con audiencias técnicas como no técnicas, incluidos el equipo y las partes interesadas dentro y fuera de la organización.\n\nSegún sea necesario, toma la iniciativa en la explicación de conceptos complejos para apoyar la toma de decisiones.\n\nEscucha y hace preguntas perspicaces para identificar diferentes perspectivas a fin de aclarar y confirmar la comprensión.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Alienta y apoya los debates en equipo sobre iniciativas de mejora.\n\nImplementa cambios de procedimiento dentro de un ámbito definido de trabajo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica, facilita y desarrolla conceptos de pensamiento creativo y encuentra formas alternativas de enfocar los resultados del equipo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza su criterio y una discreción sustancial para identificar y responder a problemas complejos y tareas relacionadas con proyectos y objetivos del equipo.\n\nEscala cuando el alcance se ve afectado.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Maximiza las capacidades de las aplicaciones para su función, y evalúa y soporta el uso de nuevas tecnologías y herramientas digitales.\n\nSelecciona adecuadamente y evalúa el impacto de los cambios en los estándares, métodos, herramientas, aplicaciones y procesos aplicables y relevantes para la propia especialidad.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Dirige, apoya o guía a los miembros del equipo.\n\nDesarrolla soluciones para actividades laborales complejas relacionadas con asignaciones. \n\nDemuestra una comprensión de los factores de riesgo en su trabajo.\n\nContribuye con conocimientos especializados a la definición de requisitos para respaldar propuestas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Absorbe rápidamente y evalúa críticamente nueva información y la aplica eficazmente.\n\nMantiene una comprensión de las prácticas emergentes y de su aplicación, y asume la responsabilidad de impulsar las oportunidades de desarrollo propias y de los miembros del equipo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Planifica, programa y supervisa el trabajo para cumplir determinados objetivos y procesos personales y/o de equipo, demostrando un enfoque analítico para cumplir alcanzar los objetivos de tiempo y calidad.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Investiga la causa y el impacto, evalúa las opciones y resuelve una amplia gama de problemas complejos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Permite a otros adaptarse y cambiar en respuesta a los desafíos y cambios en el entorno de trabajo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Adapta y aplica las normas aplicables, reconociendo su importancia en la consecución de resultados en equipo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 5, + "title": "Asegurar, asesorar", + "guiding_phrase": "Asegurar, asesorar", + "essence": "Esencia del nivel: Proporciona orientación autorizada en su campo y trabaja bajo una dirección amplia. Responsable de entregar resultados de trabajo significativos, desde el análisis hasta la ejecución y evaluación.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-5", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomía", + "description": "Trabaja bajo una dirección amplia. El trabajo es de iniciativa propia, coherente con los requisitos operativos y presupuestarios acordados para cumplir los objetivos técnicos y/o grupales asignados. Define tareas y delega el trabajo en equipos e individuos dentro de su área de responsabilidad.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influencia", + "description": "Influye en las decisiones críticas bajo su dominio. Mantiene contacto a nivel operativo que impacta en la ejecución e implementación con colegas internos y contactos externos. Tiene influencia considerable en la asignación y gestión de los recursos requeridos para entregar los proyectos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complejidad", + "description": "Realiza una amplia gama de actividades complejas de trabajo técnico y/o profesional, que requieren la aplicación de principios fundamentales en una gama de contextos impredecibles.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conocimiento", + "description": "Aplica conocimientos para interpretar situaciones complejas y ofrecer asesoramiento autorizado. Aplica una experiencia profunda en campos específicos, con una comprensión más amplia en toda la industria o negocio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Facilita la colaboración entre las partes interesadas que tienen diversos objetivos.\n\nGarantiza formas de trabajo colaborativas en todas las etapas del trabajo para satisfacer las necesidades del usuario/cliente.\n\nConstruye relaciones efectivas en toda la organización y con clientes, proveedores y socios.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Comunica claramente y con impacto, articulando información e ideas complejas para audiencias amplias con diferentes puntos de vista.\n\nLidera y fomenta conversaciones para compartir ideas y generar consenso sobre las medidas a tomar.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Identifica y evalúa mejoras potenciales de productos, prácticas o servicios.\n\nDirige la aplicación de mejoras dentro de su propia área de responsabilidades.\n\nEvalúa la efectividad de los cambios implementados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica de manera creativa el pensamiento innovador y prácticas de diseño para identificar soluciones que le den valor en beneficio de clientes y partes interesadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza el buen juicio para tomar decisiones informadas sobre las acciones para lograr resultados organizacionales, como el cumplimiento de metas, plazos y presupuestos.\n\nPlantea cuestionamientos cuando los objetivos están en riesgo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Reconoce y evalúa el impacto organizacional de las nuevas tecnologías y servicios digitales.\n\nImplementa prácticas nuevas y efectivas. \n\nAsesora sobre los estándares, métodos, herramientas, aplicaciones y procesos disponibles y relevantes para especialidades grupales, y puede tomar decisiones apropiadas a partir de las alternativas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Proporciona liderazgo a nivel operativo.\n\nImplementa y ejecuta políticas alineadas con los planes estratégicos.\n\nEstima y evalúa riesgos.\n\nAl examinar propuestas toma en cuenta todos los requisitos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Utiliza sus habilidades y conocimientos para ayudar a establecer los estándares que otros en la organización aplicarán.\n\nToma la iniciativa de desarrollar una mayor amplitud de conocimientos en toda la industria y/o negocio, así como para identificar y gestionar oportunidades de desarrollo en su área de responsabilidad.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Analiza, diseña, planifica, establece hitos y ejecuta y evalúa el trabajo según los objetivos de tiempo, costo y calidad.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Investiga cuestiones complejas para identificar las causas fundamentales y los impactos, evalúa una variedad de soluciones y toma decisiones informadas sobre el mejor curso de acción, a menudo en colaboración con otros expertos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Lidera adaptaciones a entornos empresariales cambiantes.\n\nGuía a los equipos a través de las transiciones, manteniendo el enfoque en los objetivos organizacionales.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Contribuye de manera proactiva a la implementación de prácticas de trabajo profesionales y ayuda a promover una cultura organizacional de apoyo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 6, + "title": "Iniciar, influir", + "guiding_phrase": "Iniciar, influir", + "essence": "Esencia del nivel: Tiene una influencia organizativa considerable, toma decisiones de alto nivel, moldea políticas, demuestra liderazgo, promueve la colaboración organizacional y acepta la rendición de cuentas en áreas clave.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-6", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomía", + "description": "Orienta las decisiones y estrategias de alto nivel dentro de las políticas y objetivos generales de la organización. Tiene autoridad y rendición de cuentas definidas por las acciones y decisiones dentro de un área significativa de trabajo, incluso aspectos técnicos, financieros y de calidad. Delega la responsabilidad de los objetivos operativos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influencia", + "description": "Influye en la formación de la estrategia y en la ejecución de los planes de negocio. Tiene un nivel gerencial significativo de contacto con colegas internos y contactos externos. Tiene liderazgo organizacional e influencia sobre el nombramiento y manejo de recursos relacionados con la implementación de iniciativas estratégicas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complejidad", + "description": "Realiza actividades laborales de alta complejidad que abarcan aspectos técnicos, financieros y de calidad.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conocimiento", + "description": "Aplica amplios conocimientos empresariales para permitir el liderazgo estratégico y la toma de decisiones en diversos ámbitos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Lidera la colaboración con una amplia gama de partes interesadas a través de objetivos contrapuestos dentro de la organización.\n\nEstablece conexiones sólidas e influyentes con contactos internos y externos fundamentales a nivel de dirección superior/líder técnico.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Comunica con credibilidad a todos los niveles y en toda la organización a audiencias amplias con objetivos divergentes.\n\nExplica claramente información e ideas complejas, influyendo en la dirección estratégica.\n\nPromueve el intercambio de información en toda la organización.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Impulsa iniciativas de mejora que tienen un impacto significativo en la organización.\n\nAlinea las estrategias de mejora con los objetivos organizacionales.\n\nInvolucra a las partes interesadas en los procesos de mejora.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Aplica creativamente una amplia gama de nuevas ideas y técnicas de gestión eficaces para lograr resultados que se alinean con la estrategia de la organización.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Utiliza su criterio para tomar decisiones que inician el logro de los objetivos estratégicos acordados, incluido el rendimiento financiero.\n\nEscala cuando se ve afectada una dirección estratégica más amplia.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Lidera la mejora de las capacidades digitales de la organización. \n\nIdentifica y respalda oportunidades para adoptar nuevas tecnologías y servicios digitales.\n\nLidera la gobernanza digital y el cumplimiento de la legislación pertinente, y la necesidad de productos y servicios.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Proporciona liderazgo a nivel organizacional.\n\nContribuye al desarrollo e implementación de políticas y estrategias.\n\nEntiende y comunica la evolución de la industria, y el rol y el impacto de la tecnología. \n\nGestiona y mitiga los riesgos organizacionales.  \n\nEquilibra los requisitos de las propuestas con las necesidades más generales de la organización.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Promueve la aplicación del conocimiento para apoyar imperativos estratégicos.\n\nDesarrolla activamente sus habilidades de liderazgo estratégico y técnico, y lidera el desarrollo de habilidades en su área de responsabilidad.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Inicia e influye en los objetivos estratégicos y asigna responsabilidades.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Anticipa y lidera la resolución de problemas y oportunidades que puedan afectar los objetivos de la organización, estableciendo un enfoque estratégico y asignando recursos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Impulsa la adaptabilidad organizacional iniciando y liderando cambios significativos. Influye en las estrategias de gestión del cambio a nivel organizacional.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Desempeña un papel de liderazgo en la promoción y garantía de una cultura y prácticas de trabajo adecuadas, incluida la igualdad de acceso y oportunidades para las personas con capacidades diversas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 7, + "title": "Establecer estrategia, inspirar, movilizar", + "guiding_phrase": "Establecer estrategia, inspirar, movilizar", + "essence": "Esencia del nivel: Opera al más alto nivel organizacional, determina la visión y la estrategia general de la organización y asume la responsabilidad por el éxito general.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-7", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomía", + "description": "Define y lidera la visión y estrategia de la organización dentro de los objetivos de negocio amplios. Es totalmente responsable de las acciones realizadas y las decisiones tomadas, tanto por sí mismo como por otros a quienes se les han asignado responsabilidades. Delega autoridad y responsabilidad en los objetivos estratégicos del negocio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influencia", + "description": "Dirige, influye e inspira la dirección estratégica y el desarrollo de la organización. Tiene un amplio nivel de liderazgo de contacto con colegas internos y contactos externos. Autoriza el nombramiento de los recursos requeridos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complejidad", + "description": "Ejerce un amplio liderazgo estratégico en la entrega de valor empresarial a través de visión, gobernanza y gestión ejecutiva.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conocimiento", + "description": "Aplica conocimientos estratégicos y de base amplia para dar forma a la estrategia organizacional, anticiparse a las tendencias futuras de la industria y preparar a la organización para adaptarse y liderar.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboración", + "description": "Impulsa la colaboración, comprometiéndose con las partes interesadas de liderazgo, para asegurar la alineación con la visión y la estrategia corporativas. \n\nConstruye relaciones fuertes e influyentes con clientes, socios y líderes de la industria.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicación", + "description": "Se comunica con audiencias de todos los niveles dentro de su propia organización y se involucra con la industria.\n\nPresenta argumentos e ideas convincentes de forma autoritaria y convincente para alcanzar los objetivos de negocio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidad de mejora", + "description": "Define y comunica el enfoque organizacional para la mejora continua.\n\nCultiva una cultura de mejora continua.\n\nEvalúa el impacto de las iniciativas de mejora en el éxito organizacional.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Creatividad", + "description": "Promueve la creatividad y la innovación para impulsar el desarrollo de estrategias que permitan oportunidades comerciales.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Toma de decisiones", + "description": "Aplica el buen juicio en la toma de decisiones críticas para la dirección estratégica de la organización y el éxito.\n\nEscala cuando se requiere la aportación de la dirección ejecutiva empresarial a través de estructuras de gobernanza establecidas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidad digital", + "description": "Lidera el desarrollo de la cultura digital de la organización y la visión transformadora.  \n\nPromueve la capacidad y/o la explotación de la tecnología dentro de una o más organizaciones a través de una comprensión profunda de la industria y las implicaciones de las tecnologías emergentes.\n\nEs responsable de evaluar cómo las leyes y reglamentaciones impactan los objetivos organizacionales y el uso de las capacidades digitales, de datos y tecnología.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderazgo", + "description": "Lidera la gestión estratégica.\n\nAplica el más alto nivel de liderazgo a la formulación e implementación de la estrategia.\n\nComunica el impacto potencial de prácticas y tecnologías emergentes en organizaciones e individuos y evalúa los riesgos de usar o no tales prácticas y tecnologías. \n\nEstablece la gobernanza para abordar los riesgos empresariales.\n\nAsegura que las propuestas se alineen con la dirección estratégica de la organización.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizaje y desarrollo", + "description": "Inspira una cultura de aprendizaje para alinearse con los objetivos de negocio.   \n\nMantiene una visión estratégica de los panoramas contemporáneos y emergentes de la industria. \n\nSe asegura de que la organización desarrolle y movilice toda la gama de capacidades y habilidades requeridas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planificación", + "description": "Planifica y lidera al más alto nivel de autoridad sobre todos los aspectos de un área de trabajo considerable.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolución de problemas", + "description": "Gestiona las interrelaciones entre las partes afectadas y los imperativos estratégicos, reconociendo el contexto empresarial más amplio y sacando conclusiones precisas al resolver problemas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidad", + "description": "Promueve la agilidad organizativa y la resiliencia.\n\nIntegra la adaptabilidad en la cultura organizacional y la planificación estratégica.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Seguridad, privacidad y ética", + "description": "Proporciona una dirección clara y un liderazgo estratégico para integrar el cumplimiento, la cultura organizacional y las prácticas de trabajo, y promueve activamente la diversidad y la inclusión.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + } + ], + "source": { + "base_url": "https://sfia-online.org/en/sfia-9/responsibilities", + "generic_attributes_url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours", + "behavioural_factors_pdf": "https://sfia-online.org/en/sfia-9/responsibilities/sfia-9-alternative-presentation-of-behavioural-factors.pdf" + } +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/es/terms-of-use.json b/projects/odilo/data/sfia-9-json/es/terms-of-use.json new file mode 100644 index 000000000..2b9405678 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/es/terms-of-use.json @@ -0,0 +1,7 @@ +{ + "type": "sfia.terms_of_use", + "sfia_version": 9, + "language": "es", + "text": null, + "note": "Terms of use sheet not present in workbook." +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/pt/attributes.json b/projects/odilo/data/sfia-9-json/pt/attributes.json new file mode 100644 index 000000000..92570cd8e --- /dev/null +++ b/projects/odilo/data/sfia-9-json/pt/attributes.json @@ -0,0 +1,743 @@ +{ + "type": "sfia.attributes", + "sfia_version": 9, + "language": "pt", + "items": [ + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "AUTO", + "name": "Autonomia", + "url": "https://sfia-online.org/pt/shortcode/9/AUTO", + "attribute_type": "Attributes", + "overall_description": "O nível de independência, liberdade e responsabilidade pelos resultados em sua função.", + "guidance_notes": "A autonomia no SFIA representa uma progressão do cumprimento de instruções para a definição da estratégia organizacional. Ela envolve:\n\ntrabalhar sob vários níveis de direção e supervisão\ntomar decisões independentes de acordo com a responsabilidade\nassumir a responsabilidade pelas ações e seus resultados\ndelegar tarefas e responsabilidades adequadamente\ndefinir metas pessoais, de equipe ou organizacionais.\n\nA autonomia efetiva engloba habilidades de tomada de decisão, autogerenciamento e a capacidade de equilibrar a independência com as metas organizacionais. A autonomia está intimamente ligada a habilidades como tomada de decisões, liderança e planejamento.\nÀ medida que os profissionais avançam, seu nível de autonomia determina cada vez mais sua capacidade de promover mudanças, inovar e contribuir para o sucesso organizacional. À medida que os profissionais avançam, sua autonomia permite que eles liderem iniciativas e gerem resultados estratégicos. Em níveis mais altos, os indivíduos moldam sua função e tomam decisões que têm um impacto organizacional mais amplo, com supervisão mínima.", + "levels": [ + { + "level": 1, + "description": "Segue as instruções e trabalha sob orientação rigorosa. Recebe instruções e orientações específicas e tem seu trabalho revisado de perto." + }, + { + "level": 2, + "description": "Trabalha sob direção rotineira. Recebe instruções e orientações e tem seu trabalho revisado regularmente." + }, + { + "level": 3, + "description": "Trabalha sob direção geral para concluir as tarefas atribuídas. Recebe orientação e tem o trabalho revisado nos marcos acordados. Quando necessário, delega tarefas de rotina a outras pessoas da própria equipe." + }, + { + "level": 4, + "description": "Trabalha sob direção geral dentro de uma estrutura clara de responsabilidade. Exerce considerável responsabilidade pessoal e autonomia. Quando necessário, planeja, agenda e delega trabalho a outras pessoas, geralmente dentro da própria equipe." + }, + { + "level": 5, + "description": "Trabalha sob ampla direção. O trabalho é de iniciativa própria, consistente com os requisitos operacionais e orçamentários acordados para atender aos objetivos técnicos e/ou de grupo alocados. Define tarefas e delega trabalho a equipes e indivíduos dentro da sua área de responsabilidade." + }, + { + "level": 6, + "description": "Orienta decisões e estratégias de alto nível dentro das políticas e dos objetivos gerais da organização. Tem autoridade e responsabilidade definidas para ações e decisões em uma área significativa de trabalho, incluindo aspectos técnicos, financeiros e de qualidade. Delega a responsabilidade pelos objetivos operacionais." + }, + { + "level": 7, + "description": "Define e lidera a visão e a estratégia da organização dentro dos objetivos comerciais mais abrangentes. É totalmente responsável pelas ações e decisões tomadas, tanto por si mesmo quanto por outros a quem foram atribuídas responsabilidades. Delega autoridade e responsabilidade pelos objetivos estratégicos do negócio." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "INFL", + "name": "Influência", + "url": "https://sfia-online.org/pt/shortcode/9/INFL", + "attribute_type": "Attributes", + "overall_description": "O alcance e o impacto de suas decisões e ações, tanto dentro quanto fora da organização.", + "guidance_notes": "A influência, no SFIA, reflete a progressão do impacto sobre os colegas imediatos para a formação da direção organizacional. Ela envolve:\n\nexpansão da esfera de interação e impacto\nprogressão de interações transacionais para interações estratégicas\nengajamento com as partes interessadas, tanto internas quanto externas, em níveis crescentes de senioridade\nmoldar decisões com impacto organizacional crescente\ncontribuir para a direção da equipe, do departamento e da organização.\n\nA influência está intimamente ligada a outros atributos, como comunicação e liderança. A influência efetiva se desenvolve por meio da experiência e da interação com níveis mais altos da organização e do setor. Esse atributo reflete o alcance e o impacto das decisões e ações, tanto dentro quanto fora da organização. \nÀ medida que os profissionais avançam, sua influência se estende para além da equipe, contribuindo para decisões estratégicas e ajudando a moldar a direção da organização. Ela progride da consciência de como o trabalho de cada um apoia os outros para direcionar a estratégia em nível organizacional. A extensão da influência geralmente se reflete na natureza das interações, no nível dos contatos e no impacto das decisões sobre a direção da organização.", + "levels": [ + { + "level": 1, + "description": "Quando necessário, contribui para discussões de equipe com colegas imediatos." + }, + { + "level": 2, + "description": "Contribui para as discussões com os membros da equipe. Trabalha ao lado dos membros da equipe, contribuindo para as decisões da equipe. Quando a função exige, interage com pessoas de fora da equipe, incluindo colegas internos e contatos externos." + }, + { + "level": 3, + "description": "Trabalha com a equipe e influencia suas decisões. Tem um nível transacional de contato com pessoas de fora da equipe, incluindo colegas internos e contatos externos." + }, + { + "level": 4, + "description": "Influencia os projetos e os objetivos da equipe. Tem um nível tático de contato com pessoas de fora da equipe, incluindo colegas internos e contatos externos." + }, + { + "level": 5, + "description": "Influencia decisões críticas em seu domínio. Tem contato de nível operacional que afeta a execução e a implementação com colegas internos e contatos externos. Tem influência significativa sobre a alocação e o gerenciamento dos recursos necessários para a execução dos projetos." + }, + { + "level": 6, + "description": "Influencia a formação da estratégia e a execução dos planos de negócios. Tem um nível gerencial significativo de contato com colegas internos e contatos externos. Exerce liderança organizacional e influência sobre a nomeação e o gerenciamento de recursos relacionados à implementação de iniciativas estratégicas." + }, + { + "level": 7, + "description": "Dirige, influencia e inspira a direção estratégica e o desenvolvimento da organização. Tem um nível de liderança extensivo de contato com colegas internos e contatos externos. Autoriza a nomeação dos recursos necessários." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "COMP", + "name": "Complexidade", + "url": "https://sfia-online.org/pt/shortcode/9/COMP", + "attribute_type": "Attributes", + "overall_description": "A variedade e a complexidade das tarefas e responsabilidades inerentes à sua função.", + "guidance_notes": "A complexidade, no SFIA, representa uma progressão das tarefas rotineiras para a liderança estratégica que proporciona valor para o negócio. Ela envolve:\n\nlidar com ambientes de trabalho cada vez mais variados e imprevisíveis\nabordar uma gama crescente de atividades técnicas ou profissionais\nresolver problemas progressivamente complexos\ngerenciamento de diversas partes interessadas\ncontribuir para a política e a estratégia\naproveitar tecnologias emergentes para gerar valor para o negócio.\n\nO gerenciamento eficaz da complexidade abrange habilidades de resolução de problemas, tomada de decisões e planejamento, além de conhecimento técnico ou profissional. Esse atributo reflete a variedade e a complexidade das tarefas e responsabilidades em uma função, progredindo de atividades rotineiras para uma liderança estratégica abrangente. Ele pode ser medido pelo nível de solução de problemas exigido, pela natureza e pelo número de participantes envolvidos e pelo impacto das decisões tomadas.\nÀ medida que os profissionais avançam, sua capacidade de navegar e aproveitar a complexidade contribui cada vez mais para a inovação organizacional, a eficiência e a vantagem competitiva.", + "levels": [ + { + "level": 1, + "description": "Realiza atividades de rotina em um ambiente estruturado." + }, + { + "level": 2, + "description": "Realiza uma série de atividades de trabalho em ambientes variados." + }, + { + "level": 3, + "description": "Realiza uma série de trabalhos, às vezes complexos e não rotineiros, em ambientes variados." + }, + { + "level": 4, + "description": "O trabalho inclui uma ampla gama de atividades técnicas ou profissionais complexas em contextos variados." + }, + { + "level": 5, + "description": "Executa uma ampla gama de atividades de trabalho técnicas e/ou profissionais complexas, exigindo a aplicação de princípios fundamentais em uma variedade de contextos imprevisíveis." + }, + { + "level": 6, + "description": "Realiza atividades de trabalho altamente complexas que abrangem aspectos técnicos, financeiros e de qualidade." + }, + { + "level": 7, + "description": "Exerce ampla liderança estratégica na entrega de valor comercial por meio de visão, governança e gerenciamento executivo." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "KNGE", + "name": "Conhecimento", + "url": "https://sfia-online.org/pt/shortcode/9/KNGE", + "attribute_type": "Attributes", + "overall_description": "A profundidade e a amplitude do entendimento necessárias para realizar e influenciar o trabalho de forma eficaz.", + "guidance_notes": "O conhecimento, no SFIA, representa uma progressão da aplicação de informações básicas específicas da função para alavancar um entendimento amplo e estratégico que molda a direção organizacional e as tendências do setor. Ele envolve:\n\naplicar conhecimentos específicos da função para realizar tarefas de rotina\nintegração de conhecimentos gerais, específicos da função e do setor\nusar o entendimento de tecnologias, métodos e processos para obter resultados\naplicar conhecimentos profundos para resolver problemas complexos\naproveitar o amplo conhecimento para influenciar decisões estratégicas\nmoldar as práticas organizacionais de gestão do conhecimento\n\nA aplicação eficaz do conhecimento se desenvolve por meio da experiência prática, da educação formal, do treinamento profissional, do aprendizado contínuo e da orientação. Ela abrange a capacidade de aplicar o conhecimento em cenários do mundo real, adaptar-se a desafios emergentes e criar valor para a organização.\nÀ medida que os profissionais avançam, sua aplicação do conhecimento evolui significativamente, desde tarefas básicas e específicas da função até a liderança organizacional estratégica. Essa progressão envolve o apoio a atividades de equipe, a aplicação de práticas em contextos de negócios, a integração de conhecimentos para tarefas complexas, a oferta de aconselhamento com autoridade e a viabilização da tomada de decisões entre domínios. Em níveis mais altos, os profissionais aplicam amplo conhecimento comercial e estratégico para moldar a estratégia organizacional e antecipar as tendências do setor.", + "levels": [ + { + "level": 1, + "description": "Aplica conhecimentos básicos para executar tarefas rotineiras, bem definidas e previsíveis, específicas da função." + }, + { + "level": 2, + "description": "Aplica o conhecimento das tarefas e práticas comuns do local de trabalho para apoiar as atividades da equipe, sob orientação." + }, + { + "level": 3, + "description": "Aplica o conhecimento de uma série de práticas específicas da função para concluir tarefas dentro de limites definidos e avalia como esse conhecimento se aplica ao contexto comercial mais amplo." + }, + { + "level": 4, + "description": "Aplica conhecimento em diferentes áreas de seu campo, integrando esse conhecimento para realizar tarefas complexas e diversas. Aplica o conhecimento prático do domínio da organização." + }, + { + "level": 5, + "description": "Aplica o conhecimento para interpretar situações complexas e oferecer conselhos com autoridade. Aplica conhecimentos profundos em campos específicos, com uma compreensão mais ampla do setor/negócio." + }, + { + "level": 6, + "description": "Aplica amplo conhecimento comercial para permitir a liderança estratégica e a tomada de decisões em vários domínios." + }, + { + "level": 7, + "description": "Aplica conhecimentos estratégicos e de base ampla para moldar a estratégia organizacional, prever tendências futuras do setor e preparar a organização para se adaptar e liderar." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "COLL", + "name": "Colaboração", + "url": "https://sfia-online.org/pt/shortcode/9/COLL", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Trabalhar de forma eficaz com outras pessoas, compartilhar recursos e coordenar esforços para atingir objetivos compartilhados.", + "guidance_notes": "A colaboração, no SFIA, representa a progressão da interação básica da equipe para parcerias estratégicas e gerenciamento de partes interessadas. Ela envolve:\n\ntrabalhar de forma cooperativa em equipes imediatas\ncompartilhamento eficaz de informações e recursos\ncoordenação de esforços para atingir objetivos comuns\nfacilitar o trabalho em equipe multifuncional\nconstruir relacionamentos influentes em toda a organização\nestabelecer e gerenciar parcerias estratégicas.\n\nA colaboração eficaz abrange a comunicação, a tomada de perspectiva e a capacidade de alinhar diversos pontos de vista a objetivos comuns. Também envolve a criação de um ambiente que estimule o compartilhamento de conhecimento e a solução coletiva de problemas.\nÀ medida que os profissionais avançam, suas habilidades de colaboração evoluem do apoio às metas da equipe para a formação da cultura organizacional, impulsionando a inovação e aumentando a capacidade da organização de enfrentar desafios complexos. Em níveis mais altos, a colaboração se estende para influenciar a cooperação e as parcerias em todo o setor.", + "levels": [ + { + "level": 1, + "description": "Trabalha principalmente em suas próprias tarefas e interage apenas com sua equipe imediata. Desenvolve a compreensão de como seu trabalho ajuda os outros." + }, + { + "level": 2, + "description": "Compreende a necessidade de colaborar com sua equipe e considera as necessidades do usuário/cliente." + }, + { + "level": 3, + "description": "Compreende e colabora com a análise das necessidades do usuário/cliente e representa isso em seu trabalho." + }, + { + "level": 4, + "description": "Facilita a colaboração entre as partes interessadas que compartilham objetivos comuns.  \nEnvolve-se e contribui para o trabalho de equipes multifuncionais para garantir que as necessidades do usuário/cliente sejam atendidas em todo o produto/escopo de trabalho." + }, + { + "level": 5, + "description": "Facilita a colaboração entre as partes interessadas que têm objetivos diferentes.\nGarante formas colaborativas de trabalho em todos os estágios do trabalho para atender às necessidades do usuário/cliente.\nEstabelece relacionamentos eficazes em toda a organização e com clientes, fornecedores e parceiros." + }, + { + "level": 6, + "description": "Lidera a colaboração com uma gama diversificada de partes interessadas em objetivos concorrentes dentro da organização.\nEstabelece conexões fortes e influentes com os principais contatos internos e externos em nível de gerência sênior/líder técnico" + }, + { + "level": 7, + "description": "Promove a colaboração, envolvendo-se com as partes interessadas da liderança, garantindo o alinhamento com a visão e a estratégia corporativas. \nEstabelece relacionamentos sólidos e influentes com clientes, parceiros e líderes do setor." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "COMM", + "name": "Comunicação", + "url": "https://sfia-online.org/pt/shortcode/9/COMM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Trocar informações, ideias e percepções de forma clara para permitir o entendimento mútuo e a cooperação.", + "guidance_notes": "A comunicação, no SFIA, representa uma progressão da interação básica da equipe para a influência complexa em toda a organização e o envolvimento externo. Ela envolve:\n\ncomunicação com as equipes imediatas\ntrocar informações e ideias com clareza\nhabilidades verbais e escritas, escuta ativa e capacidade de usar ferramentas e plataformas de comunicação adequadamente\nadaptação do estilo de comunicação a diversos públicos, tanto técnicos quanto não técnicos\narticular conceitos complexos de forma a permitir a tomada de decisões informadas\ninfluenciar a estratégia por meio de um diálogo eficaz com as partes interessadas sênior.\n\nÀ medida que os profissionais avançam, suas habilidades de comunicação evoluem do simples compartilhamento de informações dentro das equipes para influenciar decisões nos níveis mais altos de uma organização. Essa progressão envolve a adaptação da comunicação a diferentes públicos, incluindo partes interessadas sênior e parceiros externos, e a formação de resultados estratégicos por meio de um diálogo eficaz. Em níveis mais altos, os profissionais assumem a responsabilidade de usar a comunicação para conduzir a direção organizacional e se envolver com líderes do setor para atingir os objetivos do negócio.", + "levels": [ + { + "level": 1, + "description": "Comunica-se com a equipe imediata para entender e cumprir as tarefas atribuídas. Observa, ouve e faz perguntas para buscar informações ou esclarecer instruções." + }, + { + "level": 2, + "description": "Comunica informações à equipe imediata e às partes interessadas diretamente relacionadas à sua função.\nOuve para obter compreensão e faz perguntas relevantes para esclarecer ou buscar mais informações." + }, + { + "level": 3, + "description": "Comunica-se com a equipe e as partes interessadas dentro e fora da organização, explicando e apresentando as informações com clareza.\nContribui para uma série de conversas relacionadas ao trabalho, ouve os outros para obter entendimento e faz perguntas relevantes para sua função." + }, + { + "level": 4, + "description": "Comunica-se com públicos técnicos e não técnicos, incluindo a equipe e as partes interessadas dentro e fora da organização.\nConforme necessário, assume a liderança na explicação de conceitos complexos para apoiar a tomada de decisões.\nOuve e faz perguntas perspicazes para identificar diferentes perspectivas e esclarecer e confirmar o entendimento." + }, + { + "level": 5, + "description": "Comunica-se com clareza e impacto, articulando informações e ideias complexas para públicos amplos com diferentes pontos de vista.\nLidera e incentiva conversas para compartilhar ideias e criar consenso sobre as ações a serem tomadas." + }, + { + "level": 6, + "description": "Comunica-se com credibilidade em todos os níveis da organização para públicos amplos com objetivos divergentes.\nExplica informações e ideias complexas com clareza, influenciando a direção estratégica.\nPromove o compartilhamento de informações em toda a organização." + }, + { + "level": 7, + "description": "Comunica-se com o público em todos os níveis da própria organização e se envolve com o setor.\nApresenta argumentos e ideias convincentes de forma objetiva para atingir os objetivos de negócio." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "IMPM", + "name": "Mentalidade de melhoria", + "url": "https://sfia-online.org/pt/shortcode/9/IMPM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Identificação contínua de oportunidades para refinar práticas de trabalho, processos, produtos ou serviços para aumentar a eficiência e o impacto.", + "guidance_notes": "Ter uma mentalidade de melhoria, no SFIA, representa uma progressão do reconhecimento de oportunidades de aprimoramento para a condução de uma cultura de otimização contínua. Isso envolve:\n\nidentificar áreas de melhoria em processos, produtos ou serviços\nimplementar mudanças para aumentar a eficiência e a eficácia\navaliar o impacto das melhorias e refinar as abordagens\nincentivar e apoiar uma mentalidade de melhoria contínua em outras pessoas\nalinhar as iniciativas de melhoria aos objetivos organizacionais\ncultivar uma cultura de aprimoramento e otimização contínuos\n\nUma mentalidade de melhoria envolve a busca proativa de oportunidades para refinar e otimizar práticas, processos, produtos e serviços de trabalho. Isso reflete a crescente responsabilidade de identificar, implementar e liderar melhorias em escopos de influência cada vez maiores.À medida que os profissionais avançam, seu foco muda da identificação de oportunidades de melhoria em suas próprias tarefas para a liderança de iniciativas de melhoria em equipes e na organização. Essa progressão inclui o aprimoramento das práticas em nível pessoal, o apoio a outras pessoas na promoção de uma cultura de otimização contínua e a garantia de que os esforços de melhoria estejam alinhados com as metas organizacionais mais amplas. Em níveis mais altos, os profissionais assumem a responsabilidade de incorporar estratégias de melhoria contínua em toda a organização, gerando impacto de longo prazo.", + "levels": [ + { + "level": 1, + "description": "Identifica oportunidades de melhoria em suas próprias tarefas. Sugere aprimoramentos básicos quando solicitado." + }, + { + "level": 2, + "description": "Propõe ideias para melhorar a própria área de trabalho.\n\nImplementa as alterações acordadas nas tarefas de trabalho atribuídas." + }, + { + "level": 3, + "description": "Identifica e implementa melhorias em sua própria área de trabalho.\nContribui para aprimoramentos de processos da equipe." + }, + { + "level": 4, + "description": "Incentiva e apoia as discussões da equipe sobre iniciativas de melhoria.\n\nImplementa mudanças processuais em um escopo de trabalho definido." + }, + { + "level": 5, + "description": "Identifica e avalia possíveis melhorias em produtos, práticas ou serviços.\nLidera a implementação de aprimoramentos em sua própria área de responsabilidade.\nAvalia a eficácia das mudanças implementadas." + }, + { + "level": 6, + "description": "Promove iniciativas de melhoria que têm um impacto significativo na organização.\nAlinha as estratégias de aprimoramento com os objetivos organizacionais.\nEnvolve as partes interessadas nos processos de aprimoramento." + }, + { + "level": 7, + "description": "Define e comunica a abordagem organizacional para a melhoria contínua.\nCultiva uma cultura de aprimoramento contínuo.\nAvalia o impacto das iniciativas de melhoria no sucesso organizacional." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "CRTY", + "name": "Criatividade", + "url": "https://sfia-online.org/pt/shortcode/9/CRTY", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Gerar e aplicar ideias inovadoras para aprimorar processos, resolver problemas e impulsionar o sucesso organizacional.", + "guidance_notes": "A criatividade, no SFIA, representa uma progressão da geração de ideias básicas para a condução da inovação estratégica. Ela envolve:\n\ngerar ideias e soluções inovadoras\naplicar o pensamento inovador para melhorar os processos\nresolver problemas complexos de forma criativa\nincentivar e facilitar o pensamento criativo de outras pessoas\ndesenvolvimento de uma cultura de inovação\nalinhar iniciativas criativas com a estratégia organizacional.\n\nA criatividade efetiva engloba o pensamento imaginativo, as habilidades de resolução de problemas e o desafio às abordagens convencionais. Ela prospera em ambientes que incentivam a tomada de riscos calculados e valorizam ideias inovadoras.\nÀ medida que os profissionais avançam, sua função muda de contribuir para processos criativos para inspirar e liderar a inovação em um nível estratégico. Essa evolução ressalta a importância cada vez maior do pensamento criativo para impulsionar o sucesso organizacional e enfrentar desafios complexos em várias disciplinas.", + "levels": [ + { + "level": 1, + "description": "Participa da geração de novas ideias quando solicitado." + }, + { + "level": 2, + "description": "Aplica o pensamento criativo para sugerir novas maneiras de abordar uma tarefa e resolver problemas." + }, + { + "level": 3, + "description": "Aplica e contribui com técnicas de pensamento criativo para contribuir com novas ideias para seu próprio trabalho e para as atividades da equipe." + }, + { + "level": 4, + "description": "Aplica, facilita e desenvolve conceitos de pensamento criativo e encontra maneiras alternativas de abordar os resultados da equipe." + }, + { + "level": 5, + "description": "Aplica de forma criativa o pensamento inovador e práticas de design na identificação de soluções que agregarão valor para o benefício do cliente/stakeholder." + }, + { + "level": 6, + "description": "Aplica com criatividade uma ampla gama de novas ideias e técnicas de gerenciamento eficazes para obter resultados que se alinham à estratégia organizacional." + }, + { + "level": 7, + "description": "Defende a criatividade e a inovação como incentivadoras do desenvolvimento da estratégia para proporcionar oportunidades de negócio." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "DECM", + "name": "Tomada de Decisão", + "url": "https://sfia-online.org/pt/shortcode/9/DECM", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Aplicar o pensamento crítico para avaliar opções, avaliar riscos e selecionar o curso de ação mais adequado.", + "guidance_notes": "A tomada de decisões, no SFIA, representa uma progressão de escolhas rotineiras para decisões estratégicas de alto impacto. Ela envolve:\n\navaliação de informações e avaliação de riscos\nequilibrar intuição e lógica\ncompreender o contexto organizacional\ndeterminar o melhor curso de ação\nassumir a responsabilidade pelos resultados.\n\nA tomada de decisão eficaz engloba habilidades analíticas e de pensamento crítico, a capacidade de avaliar riscos e consequências e uma compreensão abrangente do contexto comercial. Também envolve saber quando escalar problemas e como equilibrar prioridades concorrentes.\nÀ medida que os profissionais avançam, sua tomada de decisões evolui da abordagem de questões rotineiras para a definição de direções estratégicas. No início, as decisões se concentram no gerenciamento de tarefas ou pequenos projetos. Com o tempo, a tomada de decisões se torna mais complexa, exigindo maior discernimento, avaliação de riscos e responsabilidade por resultados de alto impacto. Em níveis mais altos, os profissionais são responsáveis por tomar decisões críticas que influenciam a estratégia e o sucesso da organização.", + "levels": [ + { + "level": 1, + "description": "Tem pouca liberdade no atendimento a consultas.  \nBusca orientação em situações inesperadas." + }, + { + "level": 2, + "description": "Tem liberdade limitada na resolução de problemas ou consultas.\nDecide quando buscar orientação em situações inesperadas." + }, + { + "level": 3, + "description": "Tem liberdade para identificar e responder a questões complexas relacionadas às suas próprias atribuições. \nDetermina quando os problemas devem ser escalados para um nível superior." + }, + { + "level": 4, + "description": "Julga e possui liberdade substancial para identificar e responder a questões e atribuições complexas relacionadas a projetos e objetivos de equipe.\nEscalona quando o escopo é afetado." + }, + { + "level": 5, + "description": "Usa o discernimento para tomar decisões informadas sobre ações para atingir os resultados organizacionais, como o cumprimento de metas, prazos e orçamento.\nLevanta questões quando os objetivos estão em risco." + }, + { + "level": 6, + "description": "Usa o discernimento para tomar decisões que iniciam a realização dos objetivos estratégicos acordados, incluindo o desempenho financeiro.\nEscalona quando a direção estratégica mais ampla é afetada." + }, + { + "level": 7, + "description": "Usa o discernimento na tomada de decisões essenciais para a direção estratégica e o sucesso da organização.\nEncaminha, quando necessário, a contribuição da gerência executiva da empresa por meio de estruturas de governança estabelecidas." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "DIGI", + "name": "Mentalidade digital", + "url": "https://sfia-online.org/pt/shortcode/9/DIGI", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Adotar e usar efetivamente ferramentas e tecnologias digitais para melhorar o desempenho e a produtividade.", + "guidance_notes": "Ter uma mentalidade digital, no SFIA, representa uma progressão da alfabetização digital básica para a condução da estratégia digital organizacional. Isso envolve:\n\ncompreensão e aplicação de tecnologias digitais\nadaptação a cenários digitais em rápida evolução\nusar ferramentas digitais, IA e dados para aprimorar os processos de trabalho\nimpulsionar a inovação e a transformação digital\ncompreender as implicações das tecnologias emergentes, incluindo a IA, e seu potencial para promover mudanças organizacionais\ngarantir a governança digital e a conformidade.\n\nUma mentalidade digital eficaz abrange o aprendizado contínuo, a adaptabilidade e a capacidade de ver como as tecnologias digitais podem transformar os modelos e as estratégias de negócios. Ela também envolve a compreensão das implicações das tecnologias emergentes e seu potencial para promover mudanças organizacionais.\nÀ medida que os profissionais avançam, sua mentalidade digital evolui do simples uso de ferramentas digitais para a formação e liderança de estratégias digitais organizacionais. No início de suas carreiras, eles se concentram na aplicação de habilidades digitais em suas funções, mas, à medida que progridem, começam a impulsionar a inovação e a usar tecnologias emergentes para transformar os processos de trabalho. Em níveis mais altos, os profissionais são responsáveis por liderar a transformação digital, garantir a conformidade com a governança digital e incorporar uma cultura digital em toda a organização.", + "levels": [ + { + "level": 1, + "description": "Possui habilidades digitais básicas para aprender e usar aplicativos, processos e ferramentas para sua função." + }, + { + "level": 2, + "description": "Possui habilidades digitais suficientes para sua função; compreende e usa métodos, ferramentas, aplicativos e processos adequados." + }, + { + "level": 3, + "description": "Explora e aplica ferramentas e habilidades digitais relevantes para sua função.\nCompreende e usa de maneira eficaz os métodos, ferramentas, aplicativos e processos apropriados." + }, + { + "level": 4, + "description": "Maximiza as capacidades das aplicações para o seu papel, avalia e apoia a utilização de novas tecnologias e ferramentas digitais.\nSeleciona adequadamente e avalia o impacto da mudança nos padrões, métodos, ferramentas, aplicativos e processos aplicáveis relevantes à própria especialidade." + }, + { + "level": 5, + "description": "Reconhece e avalia o impacto organizacional de novas tecnologias e serviços digitais.\nImplementa práticas novas e eficazes. \nAconselha sobre os padrões, métodos, ferramentas, aplicações e processos disponíveis relevantes para a(s) especialidade(s) do grupo e pode fazer escolhas apropriadas entre alternativas." + }, + { + "level": 6, + "description": "Lidera o aprimoramento dos recursos digitais da organização. \nIdentifica e endossa oportunidades para adotar novas tecnologias e serviços digitais.\nLidera a governança digital e a conformidade com a legislação pertinente e a necessidade de produtos e serviços." + }, + { + "level": 7, + "description": "Lidera o desenvolvimento da cultura digital da organização e a visão transformacional.  \nAumenta a capacidade e/ou a exploração da tecnologia em uma ou mais organizações por meio de um profundo entendimento do setor e das implicações das tecnologias emergentes.\nResponsabiliza-se por avaliar como as leis e os regulamentos afetam os objetivos organizacionais e seu uso de recursos digitais, de dados e de tecnologia." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "LEAD", + "name": "Liderança", + "url": "https://sfia-online.org/pt/shortcode/9/LEAD", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Orientar e influenciar indivíduos ou equipes para alinhar ações com metas estratégicas e gerar resultados positivos.", + "guidance_notes": "A liderança na SFIA representa uma progressão da autogestão para a formação da estratégia organizacional. Ela envolve:\n\ndemonstração de responsabilidade pessoal\nassumir a responsabilidade pelo trabalho e pelo desenvolvimento\norientar e influenciar outras pessoas\ncontribuir para as capacidades da equipe\nalinhar as ações com os objetivos organizacionais\ninspirar e promover mudanças positivas.\n\nA liderança eficaz abrange o autoconhecimento, a influência, a compreensão, a inspiração e a motivação de outras pessoas. Também envolve pensamento estratégico, gerenciamento de riscos e a capacidade de alinhar ações com objetivos de longo prazo.\nÀ medida que os profissionais avançam, sua liderança evolui da gestão de responsabilidades pessoais para a orientação de equipes e, por fim, para a formação da estratégia organizacional. Com o passar do tempo, eles vão além de influenciar as equipes e passam a conduzir resultados estratégicos, alinhar políticas com metas organizacionais e gerenciar riscos em uma escala mais ampla. Em níveis mais altos, a liderança desempenha um papel fundamental na formação da cultura organizacional, na promoção da inovação e no aprimoramento da capacidade da organização de enfrentar desafios complexos e aproveitar oportunidades.", + "levels": [ + { + "level": 1, + "description": "Aumenta proativamente a compreensão de suas tarefas e responsabilidades no trabalho." + }, + { + "level": 2, + "description": "Assume a responsabilidade pelo desenvolvimento de sua experiência de trabalho." + }, + { + "level": 3, + "description": "Fornece orientação básica e suporte a membros menos experientes da equipe, conforme necessário." + }, + { + "level": 4, + "description": "Lidera, apoia ou orienta os membros da equipe.\nDesenvolve soluções para atividades de trabalho complexas relacionadas a atribuições. \nDemonstra compreensão dos fatores de risco em seu trabalho.\nContribui com conhecimento especializado para a definição de requisitos em apoio a propostas." + }, + { + "level": 5, + "description": "Oferece liderança em nível operacional.\nImplementa e executa políticas alinhadas aos planos estratégicos.\nAnalisa e avalia riscos.\nLeva em conta todos os requisitos ao considerar as propostas." + }, + { + "level": 6, + "description": "Oferece liderança em nível organizacional.\nContribui para o desenvolvimento e a implementação de políticas e estratégias.\nCompreende e comunica os desenvolvimentos da indústria e o papel e o impacto da tecnologia. \nGerencia e mitiga o risco organizacional.  \nEquilibra os requisitos das propostas com as necessidades mais amplas da organização." + }, + { + "level": 7, + "description": "Lidera o gerenciamento estratégico.\nAplica o mais alto nível de liderança à formulação e implementação da estratégia.\nComunica o impacto potencial das práticas emergentes e tecnologias sobre as organizações e indivíduos, e avalia os riscos de usar ou não usar tais práticas e tecnologias. \nEstabelece governança para tratar dos riscos do negócio.\nGarante que as propostas se alinhem à direção estratégica da organização." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "url": "https://sfia-online.org/pt/shortcode/9/LADV", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Adquirir continuamente novos conhecimentos e habilidades para aprimorar o desempenho pessoal e organizacional.", + "guidance_notes": "O aprendizado e o desenvolvimento profissional, no SFIA, representam uma progressão do aprimoramento das habilidades pessoais para a formação de uma cultura de aprendizado organizacional. Isso envolve:\n\nadquirir e aplicar novos conhecimentos\nidentificar e tratar lacunas de habilidades\ncompartilhar aprendizados com colegas\npromover o desenvolvimento pessoal e da equipe\npromover a aplicação do conhecimento para objetivos estratégicos\ninspirar uma cultura de aprendizagem alinhada aos objetivos do negócio.\n\nO aprendizado eficaz e o desenvolvimento profissional abrangem a educação formal, o aprendizado experimental, o estudo autodirigido e a capacidade de avaliar criticamente e aplicar novas informações. Também envolve a manutenção da conscientização sobre práticas emergentes e tendências do setor e o alinhamento de iniciativas de aprendizado com objetivos comerciais estratégicos.À medida que os profissionais avançam, sua abordagem de aprendizado e desenvolvimento evolui, passando do foco no aprimoramento de habilidades pessoais para a condução do desenvolvimento organizacional e da equipe. Com o tempo, eles deixam de aplicar novos conhecimentos e passam a liderar esforços que moldam uma cultura de aprendizado, alinhando iniciativas de desenvolvimento com metas estratégicas. Nos níveis sênior, os profissionais não apenas inspiram uma cultura de aprendizagem, mas também garantem que a organização tenha as habilidades e capacidades necessárias para navegar pelas mudanças do setor e aproveitar as oportunidades.", + "levels": [ + { + "level": 1, + "description": "Aplica o conhecimento recém-adquirido para desenvolver habilidades para sua função. Contribui para identificar as próprias oportunidades de desenvolvimento." + }, + { + "level": 2, + "description": "Absorve e aplica novas informações às tarefas.\nReconhece as habilidades pessoais e as lacunas de conhecimento e busca oportunidades de aprendizado para resolvê-las." + }, + { + "level": 3, + "description": "Absorve e aplica novas informações de forma eficaz, com a capacidade de compartilhar o aprendizado com os colegas.\nToma a iniciativa de identificar e negociar suas próprias oportunidades de desenvolvimento apropriadas." + }, + { + "level": 4, + "description": "Absorve rapidamente e avalia criticamente novas informações e as aplica de forma eficaz.\nMantém um entendimento das práticas emergentes e de sua aplicação e assume a responsabilidade de promover oportunidades de desenvolvimento para si mesmo e para os membros da equipe." + }, + { + "level": 5, + "description": "Usa suas habilidades e conhecimentos para ajudar a estabelecer os padrões que serão aplicados por outras pessoas na organização.\nToma a iniciativa de desenvolver um conhecimento mais amplo do setor e/ou da empresa e de identificar e gerenciar oportunidades de desenvolvimento na área de responsabilidade." + }, + { + "level": 6, + "description": "Promove a aplicação do conhecimento para apoiar os imperativos estratégicos.\nDesenvolve ativamente suas habilidades de liderança estratégica e técnica e lidera o desenvolvimento de habilidades em sua área de responsabilidade." + }, + { + "level": 7, + "description": "Inspira uma cultura de aprendizado para alinhar-se aos objetivos comerciais.   \nMantém uma visão estratégica dos cenários contemporâneos e emergentes do setor. \nGarante que a organização desenvolva e mobilize toda a gama de habilidades e capacidades necessárias." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "PLAN", + "name": "Planejamento", + "url": "https://sfia-online.org/pt/shortcode/9/PLAN", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Adotar uma abordagem sistemática para organizar tarefas, recursos e cronogramas para atingir metas definidas.", + "guidance_notes": "O planejamento, no SFIA, representa uma progressão da organização do trabalho individual para a liderança do planejamento estratégico em uma organização. Ele envolve: \n\ndefinição de objetivos e determinação de cronogramas\norganização de tarefas e alocação de recursos\nalinhar atividades com metas mais abrangentes\nadaptar planos a circunstâncias variáveis\nmonitorar o progresso e avaliação dos resultados\niniciar e influenciar objetivos estratégicos.\n\nO planejamento eficaz abrange habilidades analíticas, previsão e a capacidade de equilibrar várias prioridades. Também envolve adaptabilidade para responder a circunstâncias variáveis e a capacidade de alinhar planos operacionais com metas estratégicas. À medida que os profissionais avançam, suas habilidades de planejamento moldam cada vez mais a direção e o desempenho da organização.\nÀ medida que os profissionais avançam, suas responsabilidades de planejamento aumentam, passando do gerenciamento de tarefas pessoais ou de equipe para a condução de esforços de planejamento organizacional. Com o tempo, eles passam da organização de seu próprio trabalho para a definição de objetivos estratégicos que moldam a direção da organização. Em níveis mais altos, os profissionais assumem a liderança no planejamento de iniciativas complexas, garantindo o alinhamento com as metas estratégicas e orientando o desempenho organizacional.", + "levels": [ + { + "level": 1, + "description": "Confirma as etapas necessárias para tarefas individuais." + }, + { + "level": 2, + "description": "Planeja seu próprio trabalho em prazos curtos e de forma organizada." + }, + { + "level": 3, + "description": "Organiza e mantém o controle do próprio trabalho (e de outros, quando necessário) para cumprir os prazos acordados." + }, + { + "level": 4, + "description": "Planeja, programa e monitora o trabalho para atender a determinados objetivos e processos pessoais e/ou da equipe, demonstrando uma abordagem analítica para cumprir metas de tempo e qualidade." + }, + { + "level": 5, + "description": "Analisa, projeta, planeja, estabelece marcos, executa e avalia o trabalho de acordo com metas de tempo, custo e qualidade." + }, + { + "level": 6, + "description": "Inicia e influencia os objetivos estratégicos e atribui responsabilidades." + }, + { + "level": 7, + "description": "Planeja e lidera, no mais alto nível de autoridade, todos os aspectos de uma área de trabalho significativa." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "PROB", + "name": "Resolução de Problemas", + "url": "https://sfia-online.org/pt/shortcode/9/PROB", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Analisar desafios, aplicar métodos lógicos e desenvolver soluções eficazes para superar obstáculos.", + "guidance_notes": "A solução de problemas, no SFIA, representa uma progressão da abordagem de questões rotineiras para o gerenciamento de desafios estratégicos. Ela envolve:\n\nreconhecer e compreender os problemas\nanalisar possíveis soluções\nimplementar resoluções eficazes\navaliar os resultados e aprender com as experiências\nantecipar e abordar possíveis problemas de forma proativa\nalinhar a solução de problemas com os objetivos organizacionais.\n\nA solução eficaz de problemas engloba o pensamento analítico, a criatividade e a capacidade de tomar decisões informadas. Ela também envolve a colaboração com especialistas de várias disciplinas, especialmente em níveis sênior.\nÀ medida que os profissionais avançam, suas responsabilidades de solução de problemas aumentam, passando da resolução de questões rotineiras para o enfrentamento de desafios complexos e estratégicos. Inicialmente, eles se concentram em abordagens metódicas para problemas cotidianos, mas, com o tempo, desenvolvem a capacidade de prever problemas, avaliar uma série de soluções e enfrentar desafios que afetam objetivos organizacionais mais amplos. Em níveis mais altos, os profissionais lideram os esforços de solução de problemas, garantindo que os desafios complexos sejam gerenciados em alinhamento com as metas de longo prazo.", + "levels": [ + { + "level": 1, + "description": "Trabalha para entender o problema e busca ajuda para resolver problemas inesperados." + }, + { + "level": 2, + "description": "Investiga e resolve problemas de rotina." + }, + { + "level": 3, + "description": "Aplica uma abordagem metódica para investigar e avaliar opções para resolver problemas rotineiros e moderadamente complexos." + }, + { + "level": 4, + "description": "Investiga a causa e o impacto, avalia as opções e resolve uma ampla gama de problemas complexos." + }, + { + "level": 5, + "description": "Investiga questões complexas para identificar as causas básicas e os impactos, avalia uma série de soluções e toma decisões informadas sobre o melhor curso de ação, muitas vezes em colaboração com outros especialistas." + }, + { + "level": 6, + "description": "Antecipa e lidera a abordagem de problemas e oportunidades que podem afetar os objetivos organizacionais, estabelecendo uma abordagem estratégica e alocando recursos." + }, + { + "level": 7, + "description": "Gerencia as inter-relações entre as partes afetadas e os imperativos estratégicos, reconhecendo o contexto comercial mais amplo e tirando conclusões precisas ao resolver problemas." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "ADAP", + "name": "Adaptabilidade", + "url": "https://sfia-online.org/pt/shortcode/9/ADAP", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Ajustar-se às mudanças e persistir em meio a desafios nos níveis pessoal, de equipe e organizacional.", + "guidance_notes": "A adaptabilidade e a resiliência, no SFIA, representam uma progressão da flexibilidade pessoal para a formação da agilidade organizacional. Isso envolve:\n\nestar aberto a mudanças e a novas formas de trabalho\najustar-se a diferentes dinâmicas de equipe e requisitos de trabalho\nadotar novos métodos e tecnologias de forma proativa\npermitir que outras pessoas se adaptem aos desafios\nliderar equipes em transições\npromover mudanças organizacionais significativas\nincorporar a adaptabilidade à cultura organizacional.\n\nA adaptabilidade e a resiliência eficazes abrangem a abertura para mudanças, o aprendizado proativo e a capacidade de manter o foco nos objetivos durante as transições. Ela também envolve o apoio a outras pessoas durante a mudança e a criação de um ambiente em que a inovação e a flexibilidade prosperem.\nÀ medida que os profissionais avançam, sua capacidade de conduzir e gerenciar mudanças molda cada vez mais a resiliência organizacional e o sucesso de longo prazo em ambientes dinâmicos.", + "levels": [ + { + "level": 1, + "description": "Aceita mudanças e está aberto a novas formas de trabalho." + }, + { + "level": 2, + "description": "Ajusta-se a diferentes dinâmicas de equipe e requisitos de trabalho.\nParticipa dos processos de adaptação da equipe." + }, + { + "level": 3, + "description": "Adapta-se e reage às mudanças e demonstra iniciativa na adoção de novos métodos ou tecnologias." + }, + { + "level": 4, + "description": "Permite que outras pessoas se adaptem e mudem em resposta a desafios e mudanças no ambiente de trabalho." + }, + { + "level": 5, + "description": "Lidera adaptações a ambientes de negócios em constante mudança.\nOrienta as equipes durante as transições, mantendo o foco nos objetivos organizacionais." + }, + { + "level": 6, + "description": "Promove a adaptabilidade organizacional ao iniciar e liderar mudanças significativas. Influencia as estratégias de gerenciamento de mudanças em nível organizacional." + }, + { + "level": 7, + "description": "Promove a agilidade e a resiliência organizacional.\nIncorpora a adaptabilidade à cultura organizacional e ao planejamento estratégico." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + }, + { + "type": "sfia.attribute", + "sfia_version": 9, + "language": "pt", + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "url": "https://sfia-online.org/pt/shortcode/9/SCPE", + "attribute_type": "Business skills/Behavioural factors", + "overall_description": "Garantir a proteção de informações confidenciais, defender a privacidade de dados e indivíduos e demonstrar conduta ética dentro e fora da organização.", + "guidance_notes": "A segurança, a privacidade e a ética, no SFIA, representam uma progressão da conscientização básica para a liderança estratégica. Isso envolve:\n\naplicar práticas profissionais de trabalho e aderir às regras organizacionais\nimplementar padrões e práticas recomendadas\npromover uma cultura de segurança, privacidade e conduta ética\nabordar desafios éticos, inclusive aqueles introduzidos por tecnologias emergentes, como a IA\ngarantir a conformidade com as leis e regulamentos relevantes\niniciativas líderes que incorporam segurança, privacidade e ética à cultura e às operações da organização.\n\nO gerenciamento eficaz da segurança, da privacidade e da ética abrange o conhecimento técnico, as habilidades de tomada de decisões éticas e a capacidade de equilibrar prioridades conflitantes. Também envolve a criação de um ambiente em que esses princípios sejam incorporados em todos os aspectos do trabalho.\nÀ medida que os profissionais avançam, espera-se que eles assumam um papel ativo na promoção do comportamento ético e na proteção de informações confidenciais em todas as áreas de trabalho. Em níveis mais altos, os indivíduos são responsáveis por desenvolver estratégias que equilibrem as necessidades operacionais com considerações éticas, garantindo a sustentabilidade e a confiança a longo prazo.", + "levels": [ + { + "level": 1, + "description": "Desenvolve o entendimento das regras e expectativas de sua função e da organização." + }, + { + "level": 2, + "description": "Tem um bom entendimento de sua função e das regras e expectativas da organização." + }, + { + "level": 3, + "description": "Aplica profissionalismo, práticas de trabalho e conhecimento adequados ao trabalho." + }, + { + "level": 4, + "description": "Adapta e aplica os padrões aplicáveis, reconhecendo sua importância para alcançar os resultados da equipe." + }, + { + "level": 5, + "description": "Contribui proativamente para a implementação de práticas profissionais de trabalho e ajuda a promover uma cultura organizacional de apoio." + }, + { + "level": 6, + "description": "Assume um papel de liderança na promoção e garantia de cultura e práticas de trabalho apropriadas, incluindo o fornecimento de acesso e oportunidades iguais para pessoas com habilidades diversas." + }, + { + "level": 7, + "description": "Fornece direção clara e liderança estratégica para incorporar a conformidade, a cultura organizacional e as práticas de trabalho, além de promover ativamente a diversidade e a inclusão." + } + ], + "source": { + "file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "sheet": "Atributos", + "source_date": null + } + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/pt/behaviour-matrix.json b/projects/odilo/data/sfia-9-json/pt/behaviour-matrix.json new file mode 100644 index 000000000..1c72c950b --- /dev/null +++ b/projects/odilo/data/sfia-9-json/pt/behaviour-matrix.json @@ -0,0 +1,895 @@ +{ + "type": "sfia.behaviour_matrix", + "sfia_version": 9, + "language": "pt", + "factors": [ + { + "code": "COLL", + "name": "Colaboração", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration", + "levels": [ + { + "level": 1, + "description": "Trabalha principalmente em suas próprias tarefas e interage apenas com sua equipe imediata. Desenvolve a compreensão de como seu trabalho ajuda os outros." + }, + { + "level": 2, + "description": "Compreende a necessidade de colaborar com sua equipe e considera as necessidades do usuário/cliente." + }, + { + "level": 3, + "description": "Compreende e colabora com a análise das necessidades do usuário/cliente e representa isso em seu trabalho." + }, + { + "level": 4, + "description": "Facilita a colaboração entre as partes interessadas que compartilham objetivos comuns.  \nEnvolve-se e contribui para o trabalho de equipes multifuncionais para garantir que as necessidades do usuário/cliente sejam atendidas em todo o produto/escopo de trabalho." + }, + { + "level": 5, + "description": "Facilita a colaboração entre as partes interessadas que têm objetivos diferentes.\nGarante formas colaborativas de trabalho em todos os estágios do trabalho para atender às necessidades do usuário/cliente.\nEstabelece relacionamentos eficazes em toda a organização e com clientes, fornecedores e parceiros." + }, + { + "level": 6, + "description": "Lidera a colaboração com uma gama diversificada de partes interessadas em objetivos concorrentes dentro da organização.\nEstabelece conexões fortes e influentes com os principais contatos internos e externos em nível de gerência sênior/líder técnico" + }, + { + "level": 7, + "description": "Promove a colaboração, envolvendo-se com as partes interessadas da liderança, garantindo o alinhamento com a visão e a estratégia corporativas. \nEstabelece relacionamentos sólidos e influentes com clientes, parceiros e líderes do setor." + } + ] + }, + { + "code": "COMM", + "name": "Comunicação", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication", + "levels": [ + { + "level": 1, + "description": "Comunica-se com a equipe imediata para entender e cumprir as tarefas atribuídas. Observa, ouve e faz perguntas para buscar informações ou esclarecer instruções." + }, + { + "level": 2, + "description": "Comunica informações à equipe imediata e às partes interessadas diretamente relacionadas à sua função.\nOuve para obter compreensão e faz perguntas relevantes para esclarecer ou buscar mais informações." + }, + { + "level": 3, + "description": "Comunica-se com a equipe e as partes interessadas dentro e fora da organização, explicando e apresentando as informações com clareza.\nContribui para uma série de conversas relacionadas ao trabalho, ouve os outros para obter entendimento e faz perguntas relevantes para sua função." + }, + { + "level": 4, + "description": "Comunica-se com públicos técnicos e não técnicos, incluindo a equipe e as partes interessadas dentro e fora da organização.\nConforme necessário, assume a liderança na explicação de conceitos complexos para apoiar a tomada de decisões.\nOuve e faz perguntas perspicazes para identificar diferentes perspectivas e esclarecer e confirmar o entendimento." + }, + { + "level": 5, + "description": "Comunica-se com clareza e impacto, articulando informações e ideias complexas para públicos amplos com diferentes pontos de vista.\nLidera e incentiva conversas para compartilhar ideias e criar consenso sobre as ações a serem tomadas." + }, + { + "level": 6, + "description": "Comunica-se com credibilidade em todos os níveis da organização para públicos amplos com objetivos divergentes.\nExplica informações e ideias complexas com clareza, influenciando a direção estratégica.\nPromove o compartilhamento de informações em toda a organização." + }, + { + "level": 7, + "description": "Comunica-se com o público em todos os níveis da própria organização e se envolve com o setor.\nApresenta argumentos e ideias convincentes de forma objetiva para atingir os objetivos de negócio." + } + ] + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement", + "levels": [ + { + "level": 1, + "description": "Identifica oportunidades de melhoria em suas próprias tarefas. Sugere aprimoramentos básicos quando solicitado." + }, + { + "level": 2, + "description": "Propõe ideias para melhorar a própria área de trabalho.\n\nImplementa as alterações acordadas nas tarefas de trabalho atribuídas." + }, + { + "level": 3, + "description": "Identifica e implementa melhorias em sua própria área de trabalho.\nContribui para aprimoramentos de processos da equipe." + }, + { + "level": 4, + "description": "Incentiva e apoia as discussões da equipe sobre iniciativas de melhoria.\n\nImplementa mudanças processuais em um escopo de trabalho definido." + }, + { + "level": 5, + "description": "Identifica e avalia possíveis melhorias em produtos, práticas ou serviços.\nLidera a implementação de aprimoramentos em sua própria área de responsabilidade.\nAvalia a eficácia das mudanças implementadas." + }, + { + "level": 6, + "description": "Promove iniciativas de melhoria que têm um impacto significativo na organização.\nAlinha as estratégias de aprimoramento com os objetivos organizacionais.\nEnvolve as partes interessadas nos processos de aprimoramento." + }, + { + "level": 7, + "description": "Define e comunica a abordagem organizacional para a melhoria contínua.\nCultiva uma cultura de aprimoramento contínuo.\nAvalia o impacto das iniciativas de melhoria no sucesso organizacional." + } + ] + }, + { + "code": "CRTY", + "name": "Criatividade", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity", + "levels": [ + { + "level": 1, + "description": "Participa da geração de novas ideias quando solicitado." + }, + { + "level": 2, + "description": "Aplica o pensamento criativo para sugerir novas maneiras de abordar uma tarefa e resolver problemas." + }, + { + "level": 3, + "description": "Aplica e contribui com técnicas de pensamento criativo para contribuir com novas ideias para seu próprio trabalho e para as atividades da equipe." + }, + { + "level": 4, + "description": "Aplica, facilita e desenvolve conceitos de pensamento criativo e encontra maneiras alternativas de abordar os resultados da equipe." + }, + { + "level": 5, + "description": "Aplica de forma criativa o pensamento inovador e práticas de design na identificação de soluções que agregarão valor para o benefício do cliente/stakeholder." + }, + { + "level": 6, + "description": "Aplica com criatividade uma ampla gama de novas ideias e técnicas de gerenciamento eficazes para obter resultados que se alinham à estratégia organizacional." + }, + { + "level": 7, + "description": "Defende a criatividade e a inovação como incentivadoras do desenvolvimento da estratégia para proporcionar oportunidades de negócio." + } + ] + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision", + "levels": [ + { + "level": 1, + "description": "Tem pouca liberdade no atendimento a consultas.  \nBusca orientação em situações inesperadas." + }, + { + "level": 2, + "description": "Tem liberdade limitada na resolução de problemas ou consultas.\nDecide quando buscar orientação em situações inesperadas." + }, + { + "level": 3, + "description": "Tem liberdade para identificar e responder a questões complexas relacionadas às suas próprias atribuições. \nDetermina quando os problemas devem ser escalados para um nível superior." + }, + { + "level": 4, + "description": "Julga e possui liberdade substancial para identificar e responder a questões e atribuições complexas relacionadas a projetos e objetivos de equipe.\nEscalona quando o escopo é afetado." + }, + { + "level": 5, + "description": "Usa o discernimento para tomar decisões informadas sobre ações para atingir os resultados organizacionais, como o cumprimento de metas, prazos e orçamento.\nLevanta questões quando os objetivos estão em risco." + }, + { + "level": 6, + "description": "Usa o discernimento para tomar decisões que iniciam a realização dos objetivos estratégicos acordados, incluindo o desempenho financeiro.\nEscalona quando a direção estratégica mais ampla é afetada." + }, + { + "level": 7, + "description": "Usa o discernimento na tomada de decisões essenciais para a direção estratégica e o sucesso da organização.\nEncaminha, quando necessário, a contribuição da gerência executiva da empresa por meio de estruturas de governança estabelecidas." + } + ] + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset", + "levels": [ + { + "level": 1, + "description": "Possui habilidades digitais básicas para aprender e usar aplicativos, processos e ferramentas para sua função." + }, + { + "level": 2, + "description": "Possui habilidades digitais suficientes para sua função; compreende e usa métodos, ferramentas, aplicativos e processos adequados." + }, + { + "level": 3, + "description": "Explora e aplica ferramentas e habilidades digitais relevantes para sua função.\nCompreende e usa de maneira eficaz os métodos, ferramentas, aplicativos e processos apropriados." + }, + { + "level": 4, + "description": "Maximiza as capacidades das aplicações para o seu papel, avalia e apoia a utilização de novas tecnologias e ferramentas digitais.\nSeleciona adequadamente e avalia o impacto da mudança nos padrões, métodos, ferramentas, aplicativos e processos aplicáveis relevantes à própria especialidade." + }, + { + "level": 5, + "description": "Reconhece e avalia o impacto organizacional de novas tecnologias e serviços digitais.\nImplementa práticas novas e eficazes. \nAconselha sobre os padrões, métodos, ferramentas, aplicações e processos disponíveis relevantes para a(s) especialidade(s) do grupo e pode fazer escolhas apropriadas entre alternativas." + }, + { + "level": 6, + "description": "Lidera o aprimoramento dos recursos digitais da organização. \nIdentifica e endossa oportunidades para adotar novas tecnologias e serviços digitais.\nLidera a governança digital e a conformidade com a legislação pertinente e a necessidade de produtos e serviços." + }, + { + "level": 7, + "description": "Lidera o desenvolvimento da cultura digital da organização e a visão transformacional.  \nAumenta a capacidade e/ou a exploração da tecnologia em uma ou mais organizações por meio de um profundo entendimento do setor e das implicações das tecnologias emergentes.\nResponsabiliza-se por avaliar como as leis e os regulamentos afetam os objetivos organizacionais e seu uso de recursos digitais, de dados e de tecnologia." + } + ] + }, + { + "code": "LEAD", + "name": "Liderança", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership", + "levels": [ + { + "level": 1, + "description": "Aumenta proativamente a compreensão de suas tarefas e responsabilidades no trabalho." + }, + { + "level": 2, + "description": "Assume a responsabilidade pelo desenvolvimento de sua experiência de trabalho." + }, + { + "level": 3, + "description": "Fornece orientação básica e suporte a membros menos experientes da equipe, conforme necessário." + }, + { + "level": 4, + "description": "Lidera, apoia ou orienta os membros da equipe.\nDesenvolve soluções para atividades de trabalho complexas relacionadas a atribuições. \nDemonstra compreensão dos fatores de risco em seu trabalho.\nContribui com conhecimento especializado para a definição de requisitos em apoio a propostas." + }, + { + "level": 5, + "description": "Oferece liderança em nível operacional.\nImplementa e executa políticas alinhadas aos planos estratégicos.\nAnalisa e avalia riscos.\nLeva em conta todos os requisitos ao considerar as propostas." + }, + { + "level": 6, + "description": "Oferece liderança em nível organizacional.\nContribui para o desenvolvimento e a implementação de políticas e estratégias.\nCompreende e comunica os desenvolvimentos da indústria e o papel e o impacto da tecnologia. \nGerencia e mitiga o risco organizacional.  \nEquilibra os requisitos das propostas com as necessidades mais amplas da organização." + }, + { + "level": 7, + "description": "Lidera o gerenciamento estratégico.\nAplica o mais alto nível de liderança à formulação e implementação da estratégia.\nComunica o impacto potencial das práticas emergentes e tecnologias sobre as organizações e indivíduos, e avalia os riscos de usar ou não usar tais práticas e tecnologias. \nEstabelece governança para tratar dos riscos do negócio.\nGarante que as propostas se alinhem à direção estratégica da organização." + } + ] + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning", + "levels": [ + { + "level": 1, + "description": "Aplica o conhecimento recém-adquirido para desenvolver habilidades para sua função. Contribui para identificar as próprias oportunidades de desenvolvimento." + }, + { + "level": 2, + "description": "Absorve e aplica novas informações às tarefas.\nReconhece as habilidades pessoais e as lacunas de conhecimento e busca oportunidades de aprendizado para resolvê-las." + }, + { + "level": 3, + "description": "Absorve e aplica novas informações de forma eficaz, com a capacidade de compartilhar o aprendizado com os colegas.\nToma a iniciativa de identificar e negociar suas próprias oportunidades de desenvolvimento apropriadas." + }, + { + "level": 4, + "description": "Absorve rapidamente e avalia criticamente novas informações e as aplica de forma eficaz.\nMantém um entendimento das práticas emergentes e de sua aplicação e assume a responsabilidade de promover oportunidades de desenvolvimento para si mesmo e para os membros da equipe." + }, + { + "level": 5, + "description": "Usa suas habilidades e conhecimentos para ajudar a estabelecer os padrões que serão aplicados por outras pessoas na organização.\nToma a iniciativa de desenvolver um conhecimento mais amplo do setor e/ou da empresa e de identificar e gerenciar oportunidades de desenvolvimento na área de responsabilidade." + }, + { + "level": 6, + "description": "Promove a aplicação do conhecimento para apoiar os imperativos estratégicos.\nDesenvolve ativamente suas habilidades de liderança estratégica e técnica e lidera o desenvolvimento de habilidades em sua área de responsabilidade." + }, + { + "level": 7, + "description": "Inspira uma cultura de aprendizado para alinhar-se aos objetivos comerciais.   \nMantém uma visão estratégica dos cenários contemporâneos e emergentes do setor. \nGarante que a organização desenvolva e mobilize toda a gama de habilidades e capacidades necessárias." + } + ] + }, + { + "code": "PLAN", + "name": "Planejamento", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning", + "levels": [ + { + "level": 1, + "description": "Confirma as etapas necessárias para tarefas individuais." + }, + { + "level": 2, + "description": "Planeja seu próprio trabalho em prazos curtos e de forma organizada." + }, + { + "level": 3, + "description": "Organiza e mantém o controle do próprio trabalho (e de outros, quando necessário) para cumprir os prazos acordados." + }, + { + "level": 4, + "description": "Planeja, programa e monitora o trabalho para atender a determinados objetivos e processos pessoais e/ou da equipe, demonstrando uma abordagem analítica para cumprir metas de tempo e qualidade." + }, + { + "level": 5, + "description": "Analisa, projeta, planeja, estabelece marcos, executa e avalia o trabalho de acordo com metas de tempo, custo e qualidade." + }, + { + "level": 6, + "description": "Inicia e influencia os objetivos estratégicos e atribui responsabilidades." + }, + { + "level": 7, + "description": "Planeja e lidera, no mais alto nível de autoridade, todos os aspectos de uma área de trabalho significativa." + } + ] + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem", + "levels": [ + { + "level": 1, + "description": "Trabalha para entender o problema e busca ajuda para resolver problemas inesperados." + }, + { + "level": 2, + "description": "Investiga e resolve problemas de rotina." + }, + { + "level": 3, + "description": "Aplica uma abordagem metódica para investigar e avaliar opções para resolver problemas rotineiros e moderadamente complexos." + }, + { + "level": 4, + "description": "Investiga a causa e o impacto, avalia as opções e resolve uma ampla gama de problemas complexos." + }, + { + "level": 5, + "description": "Investiga questões complexas para identificar as causas básicas e os impactos, avalia uma série de soluções e toma decisões informadas sobre o melhor curso de ação, muitas vezes em colaboração com outros especialistas." + }, + { + "level": 6, + "description": "Antecipa e lidera a abordagem de problemas e oportunidades que podem afetar os objetivos organizacionais, estabelecendo uma abordagem estratégica e alocando recursos." + }, + { + "level": 7, + "description": "Gerencia as inter-relações entre as partes afetadas e os imperativos estratégicos, reconhecendo o contexto comercial mais amplo e tirando conclusões precisas ao resolver problemas." + } + ] + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability", + "levels": [ + { + "level": 1, + "description": "Aceita mudanças e está aberto a novas formas de trabalho." + }, + { + "level": 2, + "description": "Ajusta-se a diferentes dinâmicas de equipe e requisitos de trabalho.\nParticipa dos processos de adaptação da equipe." + }, + { + "level": 3, + "description": "Adapta-se e reage às mudanças e demonstra iniciativa na adoção de novos métodos ou tecnologias." + }, + { + "level": 4, + "description": "Permite que outras pessoas se adaptem e mudem em resposta a desafios e mudanças no ambiente de trabalho." + }, + { + "level": 5, + "description": "Lidera adaptações a ambientes de negócios em constante mudança.\nOrienta as equipes durante as transições, mantendo o foco nos objetivos organizacionais." + }, + { + "level": 6, + "description": "Promove a adaptabilidade organizacional ao iniciar e liderar mudanças significativas. Influencia as estratégias de gerenciamento de mudanças em nível organizacional." + }, + { + "level": 7, + "description": "Promove a agilidade e a resiliência organizacional.\nIncorpora a adaptabilidade à cultura organizacional e ao planejamento estratégico." + } + ] + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security", + "levels": [ + { + "level": 1, + "description": "Desenvolve o entendimento das regras e expectativas de sua função e da organização." + }, + { + "level": 2, + "description": "Tem um bom entendimento de sua função e das regras e expectativas da organização." + }, + { + "level": 3, + "description": "Aplica profissionalismo, práticas de trabalho e conhecimento adequados ao trabalho." + }, + { + "level": 4, + "description": "Adapta e aplica os padrões aplicáveis, reconhecendo sua importância para alcançar os resultados da equipe." + }, + { + "level": 5, + "description": "Contribui proativamente para a implementação de práticas profissionais de trabalho e ajuda a promover uma cultura organizacional de apoio." + }, + { + "level": 6, + "description": "Assume um papel de liderança na promoção e garantia de cultura e práticas de trabalho apropriadas, incluindo o fornecimento de acesso e oportunidades iguais para pessoas com habilidades diversas." + }, + { + "level": 7, + "description": "Fornece direção clara e liderança estratégica para incorporar a conformidade, a cultura organizacional e as práticas de trabalho, além de promover ativamente a diversidade e a inclusão." + } + ] + } + ], + "by_level": [ + { + "level": 1, + "title": "Segue", + "factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Trabalha principalmente em suas próprias tarefas e interage apenas com sua equipe imediata. Desenvolve a compreensão de como seu trabalho ajuda os outros." + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com a equipe imediata para entender e cumprir as tarefas atribuídas. Observa, ouve e faz perguntas para buscar informações ou esclarecer instruções." + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Identifica oportunidades de melhoria em suas próprias tarefas. Sugere aprimoramentos básicos quando solicitado." + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Participa da geração de novas ideias quando solicitado." + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Tem pouca liberdade no atendimento a consultas.  \nBusca orientação em situações inesperadas." + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Possui habilidades digitais básicas para aprender e usar aplicativos, processos e ferramentas para sua função." + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Aumenta proativamente a compreensão de suas tarefas e responsabilidades no trabalho." + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Aplica o conhecimento recém-adquirido para desenvolver habilidades para sua função. Contribui para identificar as próprias oportunidades de desenvolvimento." + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Confirma as etapas necessárias para tarefas individuais." + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Trabalha para entender o problema e busca ajuda para resolver problemas inesperados." + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Aceita mudanças e está aberto a novas formas de trabalho." + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Desenvolve o entendimento das regras e expectativas de sua função e da organização." + } + ] + }, + { + "level": 2, + "title": "Auxilia", + "factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Compreende a necessidade de colaborar com sua equipe e considera as necessidades do usuário/cliente." + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica informações à equipe imediata e às partes interessadas diretamente relacionadas à sua função.\nOuve para obter compreensão e faz perguntas relevantes para esclarecer ou buscar mais informações." + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Propõe ideias para melhorar a própria área de trabalho.\n\nImplementa as alterações acordadas nas tarefas de trabalho atribuídas." + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica o pensamento criativo para sugerir novas maneiras de abordar uma tarefa e resolver problemas." + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Tem liberdade limitada na resolução de problemas ou consultas.\nDecide quando buscar orientação em situações inesperadas." + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Possui habilidades digitais suficientes para sua função; compreende e usa métodos, ferramentas, aplicativos e processos adequados." + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Assume a responsabilidade pelo desenvolvimento de sua experiência de trabalho." + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Absorve e aplica novas informações às tarefas.\nReconhece as habilidades pessoais e as lacunas de conhecimento e busca oportunidades de aprendizado para resolvê-las." + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Planeja seu próprio trabalho em prazos curtos e de forma organizada." + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Investiga e resolve problemas de rotina." + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Ajusta-se a diferentes dinâmicas de equipe e requisitos de trabalho.\nParticipa dos processos de adaptação da equipe." + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Tem um bom entendimento de sua função e das regras e expectativas da organização." + } + ] + }, + { + "level": 3, + "title": "Aplica", + "factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Compreende e colabora com a análise das necessidades do usuário/cliente e representa isso em seu trabalho." + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com a equipe e as partes interessadas dentro e fora da organização, explicando e apresentando as informações com clareza.\nContribui para uma série de conversas relacionadas ao trabalho, ouve os outros para obter entendimento e faz perguntas relevantes para sua função." + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Identifica e implementa melhorias em sua própria área de trabalho.\nContribui para aprimoramentos de processos da equipe." + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica e contribui com técnicas de pensamento criativo para contribuir com novas ideias para seu próprio trabalho e para as atividades da equipe." + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Tem liberdade para identificar e responder a questões complexas relacionadas às suas próprias atribuições. \nDetermina quando os problemas devem ser escalados para um nível superior." + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Explora e aplica ferramentas e habilidades digitais relevantes para sua função.\nCompreende e usa de maneira eficaz os métodos, ferramentas, aplicativos e processos apropriados." + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Fornece orientação básica e suporte a membros menos experientes da equipe, conforme necessário." + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Absorve e aplica novas informações de forma eficaz, com a capacidade de compartilhar o aprendizado com os colegas.\nToma a iniciativa de identificar e negociar suas próprias oportunidades de desenvolvimento apropriadas." + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Organiza e mantém o controle do próprio trabalho (e de outros, quando necessário) para cumprir os prazos acordados." + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Aplica uma abordagem metódica para investigar e avaliar opções para resolver problemas rotineiros e moderadamente complexos." + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Adapta-se e reage às mudanças e demonstra iniciativa na adoção de novos métodos ou tecnologias." + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Aplica profissionalismo, práticas de trabalho e conhecimento adequados ao trabalho." + } + ] + }, + { + "level": 4, + "title": "Possibilita", + "factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Facilita a colaboração entre as partes interessadas que compartilham objetivos comuns.  \nEnvolve-se e contribui para o trabalho de equipes multifuncionais para garantir que as necessidades do usuário/cliente sejam atendidas em todo o produto/escopo de trabalho." + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com públicos técnicos e não técnicos, incluindo a equipe e as partes interessadas dentro e fora da organização.\nConforme necessário, assume a liderança na explicação de conceitos complexos para apoiar a tomada de decisões.\nOuve e faz perguntas perspicazes para identificar diferentes perspectivas e esclarecer e confirmar o entendimento." + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Incentiva e apoia as discussões da equipe sobre iniciativas de melhoria.\n\nImplementa mudanças processuais em um escopo de trabalho definido." + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica, facilita e desenvolve conceitos de pensamento criativo e encontra maneiras alternativas de abordar os resultados da equipe." + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Julga e possui liberdade substancial para identificar e responder a questões e atribuições complexas relacionadas a projetos e objetivos de equipe.\nEscalona quando o escopo é afetado." + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Maximiza as capacidades das aplicações para o seu papel, avalia e apoia a utilização de novas tecnologias e ferramentas digitais.\nSeleciona adequadamente e avalia o impacto da mudança nos padrões, métodos, ferramentas, aplicativos e processos aplicáveis relevantes à própria especialidade." + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Lidera, apoia ou orienta os membros da equipe.\nDesenvolve soluções para atividades de trabalho complexas relacionadas a atribuições. \nDemonstra compreensão dos fatores de risco em seu trabalho.\nContribui com conhecimento especializado para a definição de requisitos em apoio a propostas." + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Absorve rapidamente e avalia criticamente novas informações e as aplica de forma eficaz.\nMantém um entendimento das práticas emergentes e de sua aplicação e assume a responsabilidade de promover oportunidades de desenvolvimento para si mesmo e para os membros da equipe." + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Planeja, programa e monitora o trabalho para atender a determinados objetivos e processos pessoais e/ou da equipe, demonstrando uma abordagem analítica para cumprir metas de tempo e qualidade." + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Investiga a causa e o impacto, avalia as opções e resolve uma ampla gama de problemas complexos." + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Permite que outras pessoas se adaptem e mudem em resposta a desafios e mudanças no ambiente de trabalho." + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Adapta e aplica os padrões aplicáveis, reconhecendo sua importância para alcançar os resultados da equipe." + } + ] + }, + { + "level": 5, + "title": "Garante, aconselha", + "factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Facilita a colaboração entre as partes interessadas que têm objetivos diferentes.\nGarante formas colaborativas de trabalho em todos os estágios do trabalho para atender às necessidades do usuário/cliente.\nEstabelece relacionamentos eficazes em toda a organização e com clientes, fornecedores e parceiros." + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com clareza e impacto, articulando informações e ideias complexas para públicos amplos com diferentes pontos de vista.\nLidera e incentiva conversas para compartilhar ideias e criar consenso sobre as ações a serem tomadas." + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Identifica e avalia possíveis melhorias em produtos, práticas ou serviços.\nLidera a implementação de aprimoramentos em sua própria área de responsabilidade.\nAvalia a eficácia das mudanças implementadas." + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica de forma criativa o pensamento inovador e práticas de design na identificação de soluções que agregarão valor para o benefício do cliente/stakeholder." + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Usa o discernimento para tomar decisões informadas sobre ações para atingir os resultados organizacionais, como o cumprimento de metas, prazos e orçamento.\nLevanta questões quando os objetivos estão em risco." + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Reconhece e avalia o impacto organizacional de novas tecnologias e serviços digitais.\nImplementa práticas novas e eficazes. \nAconselha sobre os padrões, métodos, ferramentas, aplicações e processos disponíveis relevantes para a(s) especialidade(s) do grupo e pode fazer escolhas apropriadas entre alternativas." + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Oferece liderança em nível operacional.\nImplementa e executa políticas alinhadas aos planos estratégicos.\nAnalisa e avalia riscos.\nLeva em conta todos os requisitos ao considerar as propostas." + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Usa suas habilidades e conhecimentos para ajudar a estabelecer os padrões que serão aplicados por outras pessoas na organização.\nToma a iniciativa de desenvolver um conhecimento mais amplo do setor e/ou da empresa e de identificar e gerenciar oportunidades de desenvolvimento na área de responsabilidade." + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Analisa, projeta, planeja, estabelece marcos, executa e avalia o trabalho de acordo com metas de tempo, custo e qualidade." + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Investiga questões complexas para identificar as causas básicas e os impactos, avalia uma série de soluções e toma decisões informadas sobre o melhor curso de ação, muitas vezes em colaboração com outros especialistas." + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Lidera adaptações a ambientes de negócios em constante mudança.\nOrienta as equipes durante as transições, mantendo o foco nos objetivos organizacionais." + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Contribui proativamente para a implementação de práticas profissionais de trabalho e ajuda a promover uma cultura organizacional de apoio." + } + ] + }, + { + "level": 6, + "title": "Inicia, influencia", + "factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Lidera a colaboração com uma gama diversificada de partes interessadas em objetivos concorrentes dentro da organização.\nEstabelece conexões fortes e influentes com os principais contatos internos e externos em nível de gerência sênior/líder técnico" + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com credibilidade em todos os níveis da organização para públicos amplos com objetivos divergentes.\nExplica informações e ideias complexas com clareza, influenciando a direção estratégica.\nPromove o compartilhamento de informações em toda a organização." + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Promove iniciativas de melhoria que têm um impacto significativo na organização.\nAlinha as estratégias de aprimoramento com os objetivos organizacionais.\nEnvolve as partes interessadas nos processos de aprimoramento." + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica com criatividade uma ampla gama de novas ideias e técnicas de gerenciamento eficazes para obter resultados que se alinham à estratégia organizacional." + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Usa o discernimento para tomar decisões que iniciam a realização dos objetivos estratégicos acordados, incluindo o desempenho financeiro.\nEscalona quando a direção estratégica mais ampla é afetada." + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Lidera o aprimoramento dos recursos digitais da organização. \nIdentifica e endossa oportunidades para adotar novas tecnologias e serviços digitais.\nLidera a governança digital e a conformidade com a legislação pertinente e a necessidade de produtos e serviços." + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Oferece liderança em nível organizacional.\nContribui para o desenvolvimento e a implementação de políticas e estratégias.\nCompreende e comunica os desenvolvimentos da indústria e o papel e o impacto da tecnologia. \nGerencia e mitiga o risco organizacional.  \nEquilibra os requisitos das propostas com as necessidades mais amplas da organização." + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Promove a aplicação do conhecimento para apoiar os imperativos estratégicos.\nDesenvolve ativamente suas habilidades de liderança estratégica e técnica e lidera o desenvolvimento de habilidades em sua área de responsabilidade." + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Inicia e influencia os objetivos estratégicos e atribui responsabilidades." + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Antecipa e lidera a abordagem de problemas e oportunidades que podem afetar os objetivos organizacionais, estabelecendo uma abordagem estratégica e alocando recursos." + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Promove a adaptabilidade organizacional ao iniciar e liderar mudanças significativas. Influencia as estratégias de gerenciamento de mudanças em nível organizacional." + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Assume um papel de liderança na promoção e garantia de cultura e práticas de trabalho apropriadas, incluindo o fornecimento de acesso e oportunidades iguais para pessoas com habilidades diversas." + } + ] + }, + { + "level": 7, + "title": "Define estrategia, inspira, mobiliza", + "factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Promove a colaboração, envolvendo-se com as partes interessadas da liderança, garantindo o alinhamento com a visão e a estratégia corporativas. \nEstabelece relacionamentos sólidos e influentes com clientes, parceiros e líderes do setor." + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com o público em todos os níveis da própria organização e se envolve com o setor.\nApresenta argumentos e ideias convincentes de forma objetiva para atingir os objetivos de negócio." + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Define e comunica a abordagem organizacional para a melhoria contínua.\nCultiva uma cultura de aprimoramento contínuo.\nAvalia o impacto das iniciativas de melhoria no sucesso organizacional." + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Defende a criatividade e a inovação como incentivadoras do desenvolvimento da estratégia para proporcionar oportunidades de negócio." + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Usa o discernimento na tomada de decisões essenciais para a direção estratégica e o sucesso da organização.\nEncaminha, quando necessário, a contribuição da gerência executiva da empresa por meio de estruturas de governança estabelecidas." + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Lidera o desenvolvimento da cultura digital da organização e a visão transformacional.  \nAumenta a capacidade e/ou a exploração da tecnologia em uma ou mais organizações por meio de um profundo entendimento do setor e das implicações das tecnologias emergentes.\nResponsabiliza-se por avaliar como as leis e os regulamentos afetam os objetivos organizacionais e seu uso de recursos digitais, de dados e de tecnologia." + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Lidera o gerenciamento estratégico.\nAplica o mais alto nível de liderança à formulação e implementação da estratégia.\nComunica o impacto potencial das práticas emergentes e tecnologias sobre as organizações e indivíduos, e avalia os riscos de usar ou não usar tais práticas e tecnologias. \nEstabelece governança para tratar dos riscos do negócio.\nGarante que as propostas se alinhem à direção estratégica da organização." + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Inspira uma cultura de aprendizado para alinhar-se aos objetivos comerciais.   \nMantém uma visão estratégica dos cenários contemporâneos e emergentes do setor. \nGarante que a organização desenvolva e mobilize toda a gama de habilidades e capacidades necessárias." + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Planeja e lidera, no mais alto nível de autoridade, todos os aspectos de uma área de trabalho significativa." + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Gerencia as inter-relações entre as partes afetadas e os imperativos estratégicos, reconhecendo o contexto comercial mais amplo e tirando conclusões precisas ao resolver problemas." + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Promove a agilidade e a resiliência organizacional.\nIncorpora a adaptabilidade à cultura organizacional e ao planejamento estratégico." + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Fornece direção clara e liderança estratégica para incorporar a conformidade, a cultura organizacional e as práticas de trabalho, além de promover ativamente a diversidade e a inclusão." + } + ] + } + ], + "source": { + "generic_attributes_url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours", + "behavioural_factors_pdf": "https://sfia-online.org/en/sfia-9/responsibilities/sfia-9-alternative-presentation-of-behavioural-factors.pdf" + } +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/pt/index.json b/projects/odilo/data/sfia-9-json/pt/index.json new file mode 100644 index 000000000..0adf8bc51 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/pt/index.json @@ -0,0 +1,1052 @@ +{ + "type": "sfia.index", + "sfia_version": 9, + "language": "pt", + "generated_at": "2026-02-02", + "source_file": "sfia_source/SFIA 9 Excel - Português/sfia-9_current-standard_pt_250124.xlsx", + "source_date": null, + "counts": { + "skills": 147, + "attributes": 16, + "levels_of_responsibility": 7 + }, + "paths": { + "skills_dir": "skills/", + "attributes": "attributes.json", + "levels_of_responsibility": "levels-of-responsibility.json", + "terms_of_use": "terms-of-use.json", + "responsibilities": "responsibilities.json", + "behaviour_matrix": "behaviour-matrix.json" + }, + "skills": [ + { + "code": "ITSP", + "name": "Planejamento estratégico", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/ITSP.json" + }, + { + "code": "ISCO", + "name": "Coordenação de sistemas de informação", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/ISCO.json" + }, + { + "code": "IRMG", + "name": "Gerenciamento de informações", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/IRMG.json" + }, + { + "code": "STPL", + "name": "Arquitetura corporativa e de negócios", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/STPL.json" + }, + { + "code": "ARCH", + "name": "Arquitetura de soluções", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/ARCH.json" + }, + { + "code": "INOV", + "name": "Gestão da Inovação", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/INOV.json" + }, + { + "code": "EMRG", + "name": "Monitoramento de tecnologias emergentes", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/EMRG.json" + }, + { + "code": "RSCH", + "name": "Pesquisa formal", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/RSCH.json" + }, + { + "code": "SUST", + "name": "Sustentabilidade", + "category": "Estratégia e arquitetura", + "subcategory": "Planejamento e estratégia", + "file": "skills/SUST.json" + }, + { + "code": "FMIT", + "name": "Gestão financeira", + "category": "Estratégia e arquitetura", + "subcategory": "Gerenciamento financeiro e de valor", + "file": "skills/FMIT.json" + }, + { + "code": "INVA", + "name": "Avaliação de investimentos", + "category": "Estratégia e arquitetura", + "subcategory": "Gerenciamento financeiro e de valor", + "file": "skills/INVA.json" + }, + { + "code": "BENM", + "name": "Gestão de benefícios", + "category": "Estratégia e arquitetura", + "subcategory": "Gerenciamento financeiro e de valor", + "file": "skills/BENM.json" + }, + { + "code": "BUDF", + "name": "Orçamento e previsão", + "category": "Estratégia e arquitetura", + "subcategory": "Gerenciamento financeiro e de valor", + "file": "skills/BUDF.json" + }, + { + "code": "FIAN", + "name": "Análise financeira", + "category": "Estratégia e arquitetura", + "subcategory": "Gerenciamento financeiro e de valor", + "file": "skills/FIAN.json" + }, + { + "code": "COMG", + "name": "Gestão de custos", + "category": "Estratégia e arquitetura", + "subcategory": "Gerenciamento financeiro e de valor", + "file": "skills/COMG.json" + }, + { + "code": "DEMM", + "name": "Gestão da demanda", + "category": "Estratégia e arquitetura", + "subcategory": "Gerenciamento financeiro e de valor", + "file": "skills/DEMM.json" + }, + { + "code": "MEAS", + "name": "Medições", + "category": "Estratégia e arquitetura", + "subcategory": "Gerenciamento financeiro e de valor", + "file": "skills/MEAS.json" + }, + { + "code": "SCTY", + "name": "Segurança da informação", + "category": "Estratégia e arquitetura", + "subcategory": "Segurança e privacidade", + "file": "skills/SCTY.json" + }, + { + "code": "INAS", + "name": "Garantia da informação", + "category": "Estratégia e arquitetura", + "subcategory": "Segurança e privacidade", + "file": "skills/INAS.json" + }, + { + "code": "THIN", + "name": "Modelagem de ameaças", + "category": "Estratégia e arquitetura", + "subcategory": "Segurança e privacidade", + "file": "skills/THIN.json" + }, + { + "code": "GOVN", + "name": "Governança", + "category": "Estratégia e arquitetura", + "subcategory": "Governança, risco e conformidade", + "file": "skills/GOVN.json" + }, + { + "code": "BURM", + "name": "Gerenciamento de riscos", + "category": "Estratégia e arquitetura", + "subcategory": "Governança, risco e conformidade", + "file": "skills/BURM.json" + }, + { + "code": "AIDE", + "name": "Inteligência Artificial (IA) e Ética de Dados", + "category": "Estratégia e arquitetura", + "subcategory": "Governança, risco e conformidade", + "file": "skills/AIDE.json" + }, + { + "code": "AUDT", + "name": "Auditoria", + "category": "Estratégia e arquitetura", + "subcategory": "Governança, risco e conformidade", + "file": "skills/AUDT.json" + }, + { + "code": "QUMG", + "name": "Gestão da qualidade", + "category": "Estratégia e arquitetura", + "subcategory": "Governança, risco e conformidade", + "file": "skills/QUMG.json" + }, + { + "code": "QUAS", + "name": "Garantia da qualidade", + "category": "Estratégia e arquitetura", + "subcategory": "Governança, risco e conformidade", + "file": "skills/QUAS.json" + }, + { + "code": "CNSL", + "name": "Consultoria", + "category": "Estratégia e arquitetura", + "subcategory": "Aconselhamento e orientação", + "file": "skills/CNSL.json" + }, + { + "code": "TECH", + "name": "Consultoria especializada", + "category": "Estratégia e arquitetura", + "subcategory": "Aconselhamento e orientação", + "file": "skills/TECH.json" + }, + { + "code": "PEDP", + "name": "Informação e conformidade de dados", + "category": "Estratégia e arquitetura", + "subcategory": "Segurança e privacidade", + "file": "skills/PEDP.json" + }, + { + "code": "METL", + "name": "Métodos e ferramentas", + "category": "Estratégia e arquitetura", + "subcategory": "Aconselhamento e orientação", + "file": "skills/METL.json" + }, + { + "code": "POMG", + "name": "Gestão de portfólio", + "category": "Mudança e transformação", + "subcategory": "Implementação da mudança nos negócios", + "file": "skills/POMG.json" + }, + { + "code": "PGMG", + "name": "Gestão de programas", + "category": "Mudança e transformação", + "subcategory": "Implementação da mudança nos negócios", + "file": "skills/PGMG.json" + }, + { + "code": "PRMG", + "name": "Gestão de projetos", + "category": "Mudança e transformação", + "subcategory": "Implementação da mudança nos negócios", + "file": "skills/PRMG.json" + }, + { + "code": "PROF", + "name": "Suporte a portfólios, programas e projetos", + "category": "Mudança e transformação", + "subcategory": "Implementação da mudança nos negócios", + "file": "skills/PROF.json" + }, + { + "code": "DEMG", + "name": "Gestão de Entrega", + "category": "Mudança e transformação", + "subcategory": "Implementação da mudança nos negócios", + "file": "skills/DEMG.json" + }, + { + "code": "BUSA", + "name": "Análise da situação do negócio", + "category": "Mudança e transformação", + "subcategory": "Análise de mudanças", + "file": "skills/BUSA.json" + }, + { + "code": "FEAS", + "name": "Avaliação de viabilidade", + "category": "Mudança e transformação", + "subcategory": "Análise de mudanças", + "file": "skills/FEAS.json" + }, + { + "code": "REQM", + "name": "Definição e gerenciamento de requisitos", + "category": "Mudança e transformação", + "subcategory": "Análise de mudanças", + "file": "skills/REQM.json" + }, + { + "code": "BSMO", + "name": "Modelagem de negócios", + "category": "Mudança e transformação", + "subcategory": "Análise de mudanças", + "file": "skills/BSMO.json" + }, + { + "code": "BPTS", + "name": "Teste de aceitação do usuário", + "category": "Mudança e transformação", + "subcategory": "Análise de mudanças", + "file": "skills/BPTS.json" + }, + { + "code": "VURE", + "name": "Pesquisa de vulnerabilidades", + "category": "Estratégia e arquitetura", + "subcategory": "Segurança e privacidade", + "file": "skills/VURE.json" + }, + { + "code": "BPRE", + "name": "Melhoria em processos de negócio", + "category": "Mudança e transformação", + "subcategory": "Planejamento de mudanças", + "file": "skills/BPRE.json" + }, + { + "code": "OCEN", + "name": "Facilitação de mudança organizacional", + "category": "Mudança e transformação", + "subcategory": "Planejamento de mudanças", + "file": "skills/OCEN.json" + }, + { + "code": "OCDV", + "name": "Desenvolvimento de capacidade organizacional", + "category": "Mudança e transformação", + "subcategory": "Planejamento de mudanças", + "file": "skills/OCDV.json" + }, + { + "code": "ORDI", + "name": "Planejamento e implementação organizacional", + "category": "Mudança e transformação", + "subcategory": "Planejamento de mudanças", + "file": "skills/ORDI.json" + }, + { + "code": "JADN", + "name": "Análise e desenho de cargos, funções e perfis", + "category": "Mudança e transformação", + "subcategory": "Planejamento de mudanças", + "file": "skills/JADN.json" + }, + { + "code": "CIPM", + "name": "Gestão de mudanças organizacionais", + "category": "Mudança e transformação", + "subcategory": "Planejamento de mudanças", + "file": "skills/CIPM.json" + }, + { + "code": "PROD", + "name": "Gestão de produtos", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/PROD.json" + }, + { + "code": "DLMG", + "name": "Gerenciamento de desenvolvimento de sistemas", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/DLMG.json" + }, + { + "code": "SLEN", + "name": "Engenharia de ciclo de vida de sistemas e software", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/SLEN.json" + }, + { + "code": "DESN", + "name": "Projeto de sistemas", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/DESN.json" + }, + { + "code": "SWDN", + "name": "Projeto de software", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/SWDN.json" + }, + { + "code": "NTDS", + "name": "Projeto de redes", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/NTDS.json" + }, + { + "code": "IFDN", + "name": "Projeto de infraestrutura", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/IFDN.json" + }, + { + "code": "HWDE", + "name": "Projeto de hardware", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/HWDE.json" + }, + { + "code": "PROG", + "name": "Programação/desenvolvimento de software", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/PROG.json" + }, + { + "code": "SINT", + "name": "Integração e construção de sistemas", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/SINT.json" + }, + { + "code": "TEST", + "name": "Testes funcionais", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/TEST.json" + }, + { + "code": "NFTS", + "name": "Testes não funcionais", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/NFTS.json" + }, + { + "code": "PRTS", + "name": "Testes de processo", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/PRTS.json" + }, + { + "code": "PORT", + "name": "Configuração de software", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/PORT.json" + }, + { + "code": "RESD", + "name": "Desenvolvimento de sistemas de tempo real/embarcado", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/RESD.json" + }, + { + "code": "SFEN", + "name": "Engenharia de segurança", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/SFEN.json" + }, + { + "code": "SFAS", + "name": "Avaliação de segurança", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/SFAS.json" + }, + { + "code": "RFEN", + "name": "Engenharia de radiofrequência", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/RFEN.json" + }, + { + "code": "ADEV", + "name": "Desenvolvimento de animações", + "category": "Desenvolvimento e implementação", + "subcategory": "Desenvolvimento de sistemas", + "file": "skills/ADEV.json" + }, + { + "code": "DATM", + "name": "Gestão de dados", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/DATM.json" + }, + { + "code": "DTAN", + "name": "Modelagem e design de dados", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/DTAN.json" + }, + { + "code": "DBDS", + "name": "Projeto de banco de dados", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/DBDS.json" + }, + { + "code": "DAAN", + "name": "Análise de dados", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/DAAN.json" + }, + { + "code": "DATS", + "name": "Ciência de dados", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/DATS.json" + }, + { + "code": "MLNG", + "name": "Aprendizagem de máquina (Machine learning)", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/MLNG.json" + }, + { + "code": "BINT", + "name": "Inteligência de negócio (BI)", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/BINT.json" + }, + { + "code": "DENG", + "name": "Engenharia de dados", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/DENG.json" + }, + { + "code": "VISL", + "name": "Visualização de dados", + "category": "Desenvolvimento e implementação", + "subcategory": "Analise de dados", + "file": "skills/VISL.json" + }, + { + "code": "URCH", + "name": "Pesquisa de usuário", + "category": "Desenvolvimento e implementação", + "subcategory": "Design Centrado no Usuário", + "file": "skills/URCH.json" + }, + { + "code": "CEXP", + "name": "Experiência do cliente", + "category": "Desenvolvimento e implementação", + "subcategory": "Design Centrado no Usuário", + "file": "skills/CEXP.json" + }, + { + "code": "ACIN", + "name": "Acessibilidade e Inclusão", + "category": "Desenvolvimento e implementação", + "subcategory": "Design Centrado no Usuário", + "file": "skills/ACIN.json" + }, + { + "code": "UNAN", + "name": "Análise da experiência do usuário", + "category": "Desenvolvimento e implementação", + "subcategory": "Design Centrado no Usuário", + "file": "skills/UNAN.json" + }, + { + "code": "HCEV", + "name": "Design da experiência do usuário", + "category": "Desenvolvimento e implementação", + "subcategory": "Design Centrado no Usuário", + "file": "skills/HCEV.json" + }, + { + "code": "USEV", + "name": "Avaliação da experiência do usuário", + "category": "Desenvolvimento e implementação", + "subcategory": "Design Centrado no Usuário", + "file": "skills/USEV.json" + }, + { + "code": "INCA", + "name": "Criação e desenho de conteúdo", + "category": "Desenvolvimento e implementação", + "subcategory": "Gerenciamento de conteúdo", + "file": "skills/INCA.json" + }, + { + "code": "ICPM", + "name": "Publicação de conteúdo", + "category": "Desenvolvimento e implementação", + "subcategory": "Gerenciamento de conteúdo", + "file": "skills/ICPM.json" + }, + { + "code": "KNOW", + "name": "Gestão do conhecimento", + "category": "Desenvolvimento e implementação", + "subcategory": "Gerenciamento de conteúdo", + "file": "skills/KNOW.json" + }, + { + "code": "GRDN", + "name": "Design gráfico", + "category": "Desenvolvimento e implementação", + "subcategory": "Gerenciamento de conteúdo", + "file": "skills/GRDN.json" + }, + { + "code": "SCMO", + "name": "Modelagem científica", + "category": "Desenvolvimento e implementação", + "subcategory": "Ciência computacional", + "file": "skills/SCMO.json" + }, + { + "code": "NUAN", + "name": "Análise numérica", + "category": "Desenvolvimento e implementação", + "subcategory": "Ciência computacional", + "file": "skills/NUAN.json" + }, + { + "code": "HPCC", + "name": "Computação de alto desempenho", + "category": "Desenvolvimento e implementação", + "subcategory": "Ciência computacional", + "file": "skills/HPCC.json" + }, + { + "code": "ITMG", + "name": "Gerenciamento de serviços de tecnologia", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/ITMG.json" + }, + { + "code": "ASUP", + "name": "Suporte a aplicações", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/ASUP.json" + }, + { + "code": "ITOP", + "name": "Operações de infraestrutura", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/ITOP.json" + }, + { + "code": "SYSP", + "name": "Administração de software de sistema", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/SYSP.json" + }, + { + "code": "NTAS", + "name": "Suporte à redes", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/NTAS.json" + }, + { + "code": "HSIN", + "name": "Instalação e remoção de sistemas", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/HSIN.json" + }, + { + "code": "CFMG", + "name": "Gestão da configuração", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/CFMG.json" + }, + { + "code": "RELM", + "name": "Gestão de liberação", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/RELM.json" + }, + { + "code": "STMG", + "name": "Gestão de armazenamento de dados", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/STMG.json" + }, + { + "code": "DCMA", + "name": "Gerenciamento de instalações", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/DCMA.json" + }, + { + "code": "SLMO", + "name": "Gestão de nível de serviço", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/SLMO.json" + }, + { + "code": "SCMG", + "name": "Gerenciamento do catálogo de serviços", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/SCMG.json" + }, + { + "code": "AVMT", + "name": "Gerenciamento da disponibilidade", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/AVMT.json" + }, + { + "code": "COPL", + "name": "Gestão da continuidade", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/COPL.json" + }, + { + "code": "CPMG", + "name": "Gestão da capacidade", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/CPMG.json" + }, + { + "code": "USUP", + "name": "Gestão de incidentes", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/USUP.json" + }, + { + "code": "PBMG", + "name": "Gerenciamento de problemas", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/PBMG.json" + }, + { + "code": "CHMG", + "name": "Controle de mudanças", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/CHMG.json" + }, + { + "code": "ASMG", + "name": "Gestão de ativos", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/ASMG.json" + }, + { + "code": "SEAC", + "name": "Aceitação de serviços", + "category": "Fornecimento e operação", + "subcategory": "Gestão de serviços", + "file": "skills/SEAC.json" + }, + { + "code": "IAMT", + "name": "Gestão de identidades e acessos", + "category": "Fornecimento e operação", + "subcategory": "Serviços de segurança", + "file": "skills/IAMT.json" + }, + { + "code": "SCAD", + "name": "Operações de segurança", + "category": "Fornecimento e operação", + "subcategory": "Serviços de segurança", + "file": "skills/SCAD.json" + }, + { + "code": "VUAS", + "name": "Avaliação de vulnerabilidades", + "category": "Fornecimento e operação", + "subcategory": "Serviços de segurança", + "file": "skills/VUAS.json" + }, + { + "code": "DGFS", + "name": "Forense digital", + "category": "Fornecimento e operação", + "subcategory": "Serviços de segurança", + "file": "skills/DGFS.json" + }, + { + "code": "CRIM", + "name": "Investigação de cibercrime", + "category": "Fornecimento e operação", + "subcategory": "Serviços de segurança", + "file": "skills/CRIM.json" + }, + { + "code": "OCOP", + "name": "Operações cibernéticas ofensivas", + "category": "Fornecimento e operação", + "subcategory": "Serviços de segurança", + "file": "skills/OCOP.json" + }, + { + "code": "PENT", + "name": "Testes de intrusão", + "category": "Fornecimento e operação", + "subcategory": "Serviços de segurança", + "file": "skills/PENT.json" + }, + { + "code": "RMGT", + "name": "Gerenciamento de registros", + "category": "Fornecimento e operação", + "subcategory": "Operações de dados e registros", + "file": "skills/RMGT.json" + }, + { + "code": "ANCC", + "name": "Classificação analítica e codificação", + "category": "Fornecimento e operação", + "subcategory": "Operações de dados e registros", + "file": "skills/ANCC.json" + }, + { + "code": "DBAD", + "name": "Administração de banco de dados", + "category": "Fornecimento e operação", + "subcategory": "Operações de dados e registros", + "file": "skills/DBAD.json" + }, + { + "code": "PEMT", + "name": "Gestão de desempenho", + "category": "Pessoas e habilidades", + "subcategory": "Gestão de Pessoas", + "file": "skills/PEMT.json" + }, + { + "code": "EEXP", + "name": "Experiência do colaborador", + "category": "Pessoas e habilidades", + "subcategory": "Gestão de Pessoas", + "file": "skills/EEXP.json" + }, + { + "code": "OFCL", + "name": "Facilitação organizacional", + "category": "Pessoas e habilidades", + "subcategory": "Gestão de Pessoas", + "file": "skills/OFCL.json" + }, + { + "code": "DEPL", + "name": "Implementação", + "category": "Fornecimento e operação", + "subcategory": "Gestão de tecnologia", + "file": "skills/DEPL.json" + }, + { + "code": "PDSV", + "name": "Desenvolvimento profissional", + "category": "Pessoas e habilidades", + "subcategory": "Gestão de Pessoas", + "file": "skills/PDSV.json" + }, + { + "code": "WFPL", + "name": "Planejamento da força de trabalho", + "category": "Pessoas e habilidades", + "subcategory": "Gestão de Pessoas", + "file": "skills/WFPL.json" + }, + { + "code": "RESC", + "name": "Recrutamento e seleção", + "category": "Pessoas e habilidades", + "subcategory": "Gestão de Pessoas", + "file": "skills/RESC.json" + }, + { + "code": "ETMG", + "name": "Gestão de treinamento e desenvolvimento", + "category": "Pessoas e habilidades", + "subcategory": "Gerenciamento de habilidades", + "file": "skills/ETMG.json" + }, + { + "code": "TMCR", + "name": "Planejamento e desenvolvimento de ensino", + "category": "Pessoas e habilidades", + "subcategory": "Gerenciamento de habilidades", + "file": "skills/TMCR.json" + }, + { + "code": "ETDL", + "name": "Entrega de ensino", + "category": "Pessoas e habilidades", + "subcategory": "Gerenciamento de habilidades", + "file": "skills/ETDL.json" + }, + { + "code": "LEDA", + "name": "Avaliação de competências", + "category": "Pessoas e habilidades", + "subcategory": "Gerenciamento de habilidades", + "file": "skills/LEDA.json" + }, + { + "code": "CSOP", + "name": "Operação de esquemas de certificação", + "category": "Pessoas e habilidades", + "subcategory": "Gerenciamento de habilidades", + "file": "skills/CSOP.json" + }, + { + "code": "TEAC", + "name": "Ensino", + "category": "Pessoas e habilidades", + "subcategory": "Gerenciamento de habilidades", + "file": "skills/TEAC.json" + }, + { + "code": "SUBF", + "name": "Desenvolvimento de conteúdo programático", + "category": "Pessoas e habilidades", + "subcategory": "Gerenciamento de habilidades", + "file": "skills/SUBF.json" + }, + { + "code": "SORC", + "name": "Desenvolvimento de fornecedores", + "category": "Relacionamento e engajamento", + "subcategory": "Gerenciamento de partes interessadas", + "file": "skills/SORC.json" + }, + { + "code": "SUPP", + "name": "Gestão de fornecedores", + "category": "Relacionamento e engajamento", + "subcategory": "Gerenciamento de partes interessadas", + "file": "skills/SUPP.json" + }, + { + "code": "ITCM", + "name": "Gestão de contratos", + "category": "Relacionamento e engajamento", + "subcategory": "Gerenciamento de partes interessadas", + "file": "skills/ITCM.json" + }, + { + "code": "RLMT", + "name": "Gestão de relacionamentos", + "category": "Relacionamento e engajamento", + "subcategory": "Gerenciamento de partes interessadas", + "file": "skills/RLMT.json" + }, + { + "code": "CSMG", + "name": "Serviços de atendimento ao cliente", + "category": "Relacionamento e engajamento", + "subcategory": "Gerenciamento de partes interessadas", + "file": "skills/CSMG.json" + }, + { + "code": "ADMN", + "name": "Administração de negócios", + "category": "Relacionamento e engajamento", + "subcategory": "Gerenciamento de partes interessadas", + "file": "skills/ADMN.json" + }, + { + "code": "BIDM", + "name": "Gestão de cotações e propostas", + "category": "Relacionamento e engajamento", + "subcategory": "Vendas e gerenciamento de propostas", + "file": "skills/BIDM.json" + }, + { + "code": "SALE", + "name": "Vendas", + "category": "Relacionamento e engajamento", + "subcategory": "Vendas e gerenciamento de propostas", + "file": "skills/SALE.json" + }, + { + "code": "SSUP", + "name": "Suporte a vendas", + "category": "Relacionamento e engajamento", + "subcategory": "Vendas e gerenciamento de propostas", + "file": "skills/SSUP.json" + }, + { + "code": "MKTG", + "name": "Gestão de Marketing", + "category": "Relacionamento e engajamento", + "subcategory": "Marketing", + "file": "skills/MKTG.json" + }, + { + "code": "MRCH", + "name": "Pesquisa de mercado", + "category": "Relacionamento e engajamento", + "subcategory": "Marketing", + "file": "skills/MRCH.json" + }, + { + "code": "BRMG", + "name": "Gestão de marca", + "category": "Relacionamento e engajamento", + "subcategory": "Marketing", + "file": "skills/BRMG.json" + }, + { + "code": "MKCM", + "name": "Gestão de campanhas de marketing", + "category": "Relacionamento e engajamento", + "subcategory": "Marketing", + "file": "skills/MKCM.json" + }, + { + "code": "CELO", + "name": "Engajamento e fidelidade do cliente", + "category": "Relacionamento e engajamento", + "subcategory": "Marketing", + "file": "skills/CELO.json" + }, + { + "code": "DIGM", + "name": "Marketing digital", + "category": "Relacionamento e engajamento", + "subcategory": "Marketing", + "file": "skills/DIGM.json" + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/pt/levels-of-responsibility.json b/projects/odilo/data/sfia-9-json/pt/levels-of-responsibility.json new file mode 100644 index 000000000..a6cb69a1d --- /dev/null +++ b/projects/odilo/data/sfia-9-json/pt/levels-of-responsibility.json @@ -0,0 +1,49 @@ +{ + "type": "sfia.levels_of_responsibility", + "sfia_version": 9, + "language": "pt", + "items": [ + { + "level": 1, + "guiding_phrase": "Segue", + "essence": "Essência do nível: Executa tarefas de rotina sob supervisão rigorosa, segue instruções e precisa de orientação para concluir seu trabalho. Aprende e aplica habilidades e conhecimentos básicos.", + "url": "https://sfia-online.org/pt/lor/9/1" + }, + { + "level": 2, + "guiding_phrase": "Auxilia", + "essence": "Essência do nível: Presta assistência a outras pessoas, trabalha sob supervisão rotineira e usa critérios próprios para resolver problemas rotineiros. Aprende ativamente por meio de treinamento e experiências no trabalho.", + "url": "https://sfia-online.org/pt/lor/9/2" + }, + { + "level": 3, + "guiding_phrase": "Aplica", + "essence": "Essência do nível: Executa tarefas variadas, às vezes complexas e não rotineiras, usando métodos e procedimentos padrão. Trabalha sob direção geral, possui alguma liberdade e gerencia seu próprio trabalho dentro dos prazos. Aprimora proativamente as habilidades e o impacto no local de trabalho.", + "url": "https://sfia-online.org/pt/lor/9/3" + }, + { + "level": 4, + "guiding_phrase": "Possibilita", + "essence": "Essência do nível: Realiza diversas atividades complexas, apoia e orienta outras pessoas, delega tarefas quando apropriado, trabalha de forma autônoma sob orientação geral e contribui com conhecimentos especializados para atingir os objetivos da equipe.", + "url": "https://sfia-online.org/pt/lor/9/4" + }, + { + "level": 5, + "guiding_phrase": "Garante, aconselha", + "essence": "Essência do nível: Fornece orientação autorizada em seu campo e trabalha sob ampla direção. Responsável pela entrega de resultados significativos do trabalho, desde a análise, passando pela execução, até a avaliação.", + "url": "https://sfia-online.org/pt/lor/9/5" + }, + { + "level": 6, + "guiding_phrase": "Inicia, influencia", + "essence": "Essência do nível: tem influência organizacional significativa, toma decisões de alto nível, molda políticas, demonstra liderança, promove a colaboração organizacional e aceita a responsabilidade em áreas-chave.", + "url": "https://sfia-online.org/pt/lor/9/6" + }, + { + "level": 7, + "guiding_phrase": "Define estratégIa, inspira, mobiliza", + "essence": "Essência do nível: Opera no mais alto nível organizacional, determina a visão e a estratégia organizacional geral e assume a responsabilidade pelo sucesso geral.", + "url": "https://sfia-online.org/pt/lor/9/7" + } + ] +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/pt/responsibilities.json b/projects/odilo/data/sfia-9-json/pt/responsibilities.json new file mode 100644 index 000000000..9984a7cc0 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/pt/responsibilities.json @@ -0,0 +1,762 @@ +{ + "type": "sfia.responsibilities", + "sfia_version": 9, + "language": "pt", + "guidance_notes": "Os niveis do SFIA representam niveis de responsabilidade no local de trabalho. Cada nivel sucessivo descreve maior impacto, responsabilidade e prestacao de contas.\n- Autonomia, influencia e complexidade sao atributos genericos que indicam o nivel de responsabilidade.\n- Habilidades de negocios e fatores comportamentais descrevem os comportamentos necessarios para ser eficaz em cada nivel.\n- O atributo de conhecimento define a profundidade e amplitude de compreensao necessarias para realizar e influenciar o trabalho de forma eficaz.\nCompreender esses atributos ajudara voce a aproveitar ao maximo o SFIA. Eles sao fundamentais para compreender e aplicar os niveis descritos nas descricoes de habilidades do SFIA.", + "levels": [ + { + "level": 1, + "title": "Segue", + "guiding_phrase": "Segue", + "essence": "Essência do nível: Executa tarefas de rotina sob supervisão rigorosa, segue instruções e precisa de orientação para concluir seu trabalho. Aprende e aplica habilidades e conhecimentos básicos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-1", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomia", + "description": "Segue as instruções e trabalha sob orientação rigorosa. Recebe instruções e orientações específicas e tem seu trabalho revisado de perto.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influência", + "description": "Quando necessário, contribui para discussões de equipe com colegas imediatos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexidade", + "description": "Realiza atividades de rotina em um ambiente estruturado.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conhecimento", + "description": "Aplica conhecimentos básicos para executar tarefas rotineiras, bem definidas e previsíveis, específicas da função.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Trabalha principalmente em suas próprias tarefas e interage apenas com sua equipe imediata. Desenvolve a compreensão de como seu trabalho ajuda os outros.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com a equipe imediata para entender e cumprir as tarefas atribuídas. Observa, ouve e faz perguntas para buscar informações ou esclarecer instruções.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Identifica oportunidades de melhoria em suas próprias tarefas. Sugere aprimoramentos básicos quando solicitado.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Participa da geração de novas ideias quando solicitado.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Tem pouca liberdade no atendimento a consultas.  \nBusca orientação em situações inesperadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Possui habilidades digitais básicas para aprender e usar aplicativos, processos e ferramentas para sua função.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Aumenta proativamente a compreensão de suas tarefas e responsabilidades no trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Aplica o conhecimento recém-adquirido para desenvolver habilidades para sua função. Contribui para identificar as próprias oportunidades de desenvolvimento.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Confirma as etapas necessárias para tarefas individuais.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Trabalha para entender o problema e busca ajuda para resolver problemas inesperados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Aceita mudanças e está aberto a novas formas de trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Desenvolve o entendimento das regras e expectativas de sua função e da organização.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 2, + "title": "Auxilia", + "guiding_phrase": "Auxilia", + "essence": "Essência do nível: Presta assistência a outras pessoas, trabalha sob supervisão rotineira e usa critérios próprios para resolver problemas rotineiros. Aprende ativamente por meio de treinamento e experiências no trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-2", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomia", + "description": "Trabalha sob direção rotineira. Recebe instruções e orientações e tem seu trabalho revisado regularmente.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influência", + "description": "Contribui para as discussões com os membros da equipe. Trabalha ao lado dos membros da equipe, contribuindo para as decisões da equipe. Quando a função exige, interage com pessoas de fora da equipe, incluindo colegas internos e contatos externos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexidade", + "description": "Realiza uma série de atividades de trabalho em ambientes variados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conhecimento", + "description": "Aplica o conhecimento das tarefas e práticas comuns do local de trabalho para apoiar as atividades da equipe, sob orientação.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Compreende a necessidade de colaborar com sua equipe e considera as necessidades do usuário/cliente.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica informações à equipe imediata e às partes interessadas diretamente relacionadas à sua função.\nOuve para obter compreensão e faz perguntas relevantes para esclarecer ou buscar mais informações.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Propõe ideias para melhorar a própria área de trabalho.\n\nImplementa as alterações acordadas nas tarefas de trabalho atribuídas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica o pensamento criativo para sugerir novas maneiras de abordar uma tarefa e resolver problemas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Tem liberdade limitada na resolução de problemas ou consultas.\nDecide quando buscar orientação em situações inesperadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Possui habilidades digitais suficientes para sua função; compreende e usa métodos, ferramentas, aplicativos e processos adequados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Assume a responsabilidade pelo desenvolvimento de sua experiência de trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Absorve e aplica novas informações às tarefas.\nReconhece as habilidades pessoais e as lacunas de conhecimento e busca oportunidades de aprendizado para resolvê-las.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Planeja seu próprio trabalho em prazos curtos e de forma organizada.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Investiga e resolve problemas de rotina.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Ajusta-se a diferentes dinâmicas de equipe e requisitos de trabalho.\nParticipa dos processos de adaptação da equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Tem um bom entendimento de sua função e das regras e expectativas da organização.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 3, + "title": "Aplica", + "guiding_phrase": "Aplica", + "essence": "Essência do nível: Executa tarefas variadas, às vezes complexas e não rotineiras, usando métodos e procedimentos padrão. Trabalha sob direção geral, possui alguma liberdade e gerencia seu próprio trabalho dentro dos prazos. Aprimora proativamente as habilidades e o impacto no local de trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-3", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomia", + "description": "Trabalha sob direção geral para concluir as tarefas atribuídas. Recebe orientação e tem o trabalho revisado nos marcos acordados. Quando necessário, delega tarefas de rotina a outras pessoas da própria equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influência", + "description": "Trabalha com a equipe e influencia suas decisões. Tem um nível transacional de contato com pessoas de fora da equipe, incluindo colegas internos e contatos externos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexidade", + "description": "Realiza uma série de trabalhos, às vezes complexos e não rotineiros, em ambientes variados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conhecimento", + "description": "Aplica o conhecimento de uma série de práticas específicas da função para concluir tarefas dentro de limites definidos e avalia como esse conhecimento se aplica ao contexto comercial mais amplo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Compreende e colabora com a análise das necessidades do usuário/cliente e representa isso em seu trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com a equipe e as partes interessadas dentro e fora da organização, explicando e apresentando as informações com clareza.\nContribui para uma série de conversas relacionadas ao trabalho, ouve os outros para obter entendimento e faz perguntas relevantes para sua função.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Identifica e implementa melhorias em sua própria área de trabalho.\nContribui para aprimoramentos de processos da equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica e contribui com técnicas de pensamento criativo para contribuir com novas ideias para seu próprio trabalho e para as atividades da equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Tem liberdade para identificar e responder a questões complexas relacionadas às suas próprias atribuições. \nDetermina quando os problemas devem ser escalados para um nível superior.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Explora e aplica ferramentas e habilidades digitais relevantes para sua função.\nCompreende e usa de maneira eficaz os métodos, ferramentas, aplicativos e processos apropriados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Fornece orientação básica e suporte a membros menos experientes da equipe, conforme necessário.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Absorve e aplica novas informações de forma eficaz, com a capacidade de compartilhar o aprendizado com os colegas.\nToma a iniciativa de identificar e negociar suas próprias oportunidades de desenvolvimento apropriadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Organiza e mantém o controle do próprio trabalho (e de outros, quando necessário) para cumprir os prazos acordados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Aplica uma abordagem metódica para investigar e avaliar opções para resolver problemas rotineiros e moderadamente complexos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Adapta-se e reage às mudanças e demonstra iniciativa na adoção de novos métodos ou tecnologias.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Aplica profissionalismo, práticas de trabalho e conhecimento adequados ao trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 4, + "title": "Possibilita", + "guiding_phrase": "Possibilita", + "essence": "Essência do nível: Realiza diversas atividades complexas, apoia e orienta outras pessoas, delega tarefas quando apropriado, trabalha de forma autônoma sob orientação geral e contribui com conhecimentos especializados para atingir os objetivos da equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-4", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomia", + "description": "Trabalha sob direção geral dentro de uma estrutura clara de responsabilidade. Exerce considerável responsabilidade pessoal e autonomia. Quando necessário, planeja, agenda e delega trabalho a outras pessoas, geralmente dentro da própria equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influência", + "description": "Influencia os projetos e os objetivos da equipe. Tem um nível tático de contato com pessoas de fora da equipe, incluindo colegas internos e contatos externos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexidade", + "description": "O trabalho inclui uma ampla gama de atividades técnicas ou profissionais complexas em contextos variados.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conhecimento", + "description": "Aplica conhecimento em diferentes áreas de seu campo, integrando esse conhecimento para realizar tarefas complexas e diversas. Aplica o conhecimento prático do domínio da organização.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Facilita a colaboração entre as partes interessadas que compartilham objetivos comuns.  \nEnvolve-se e contribui para o trabalho de equipes multifuncionais para garantir que as necessidades do usuário/cliente sejam atendidas em todo o produto/escopo de trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com públicos técnicos e não técnicos, incluindo a equipe e as partes interessadas dentro e fora da organização.\nConforme necessário, assume a liderança na explicação de conceitos complexos para apoiar a tomada de decisões.\nOuve e faz perguntas perspicazes para identificar diferentes perspectivas e esclarecer e confirmar o entendimento.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Incentiva e apoia as discussões da equipe sobre iniciativas de melhoria.\n\nImplementa mudanças processuais em um escopo de trabalho definido.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica, facilita e desenvolve conceitos de pensamento criativo e encontra maneiras alternativas de abordar os resultados da equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Julga e possui liberdade substancial para identificar e responder a questões e atribuições complexas relacionadas a projetos e objetivos de equipe.\nEscalona quando o escopo é afetado.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Maximiza as capacidades das aplicações para o seu papel, avalia e apoia a utilização de novas tecnologias e ferramentas digitais.\nSeleciona adequadamente e avalia o impacto da mudança nos padrões, métodos, ferramentas, aplicativos e processos aplicáveis relevantes à própria especialidade.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Lidera, apoia ou orienta os membros da equipe.\nDesenvolve soluções para atividades de trabalho complexas relacionadas a atribuições. \nDemonstra compreensão dos fatores de risco em seu trabalho.\nContribui com conhecimento especializado para a definição de requisitos em apoio a propostas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Absorve rapidamente e avalia criticamente novas informações e as aplica de forma eficaz.\nMantém um entendimento das práticas emergentes e de sua aplicação e assume a responsabilidade de promover oportunidades de desenvolvimento para si mesmo e para os membros da equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Planeja, programa e monitora o trabalho para atender a determinados objetivos e processos pessoais e/ou da equipe, demonstrando uma abordagem analítica para cumprir metas de tempo e qualidade.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Investiga a causa e o impacto, avalia as opções e resolve uma ampla gama de problemas complexos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Permite que outras pessoas se adaptem e mudem em resposta a desafios e mudanças no ambiente de trabalho.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Adapta e aplica os padrões aplicáveis, reconhecendo sua importância para alcançar os resultados da equipe.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 5, + "title": "Garante, aconselha", + "guiding_phrase": "Garante, aconselha", + "essence": "Essência do nível: Fornece orientação autorizada em seu campo e trabalha sob ampla direção. Responsável pela entrega de resultados significativos do trabalho, desde a análise, passando pela execução, até a avaliação.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-5", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomia", + "description": "Trabalha sob ampla direção. O trabalho é de iniciativa própria, consistente com os requisitos operacionais e orçamentários acordados para atender aos objetivos técnicos e/ou de grupo alocados. Define tarefas e delega trabalho a equipes e indivíduos dentro da sua área de responsabilidade.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influência", + "description": "Influencia decisões críticas em seu domínio. Tem contato de nível operacional que afeta a execução e a implementação com colegas internos e contatos externos. Tem influência significativa sobre a alocação e o gerenciamento dos recursos necessários para a execução dos projetos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexidade", + "description": "Executa uma ampla gama de atividades de trabalho técnicas e/ou profissionais complexas, exigindo a aplicação de princípios fundamentais em uma variedade de contextos imprevisíveis.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conhecimento", + "description": "Aplica o conhecimento para interpretar situações complexas e oferecer conselhos com autoridade. Aplica conhecimentos profundos em campos específicos, com uma compreensão mais ampla do setor/negócio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Facilita a colaboração entre as partes interessadas que têm objetivos diferentes.\nGarante formas colaborativas de trabalho em todos os estágios do trabalho para atender às necessidades do usuário/cliente.\nEstabelece relacionamentos eficazes em toda a organização e com clientes, fornecedores e parceiros.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com clareza e impacto, articulando informações e ideias complexas para públicos amplos com diferentes pontos de vista.\nLidera e incentiva conversas para compartilhar ideias e criar consenso sobre as ações a serem tomadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Identifica e avalia possíveis melhorias em produtos, práticas ou serviços.\nLidera a implementação de aprimoramentos em sua própria área de responsabilidade.\nAvalia a eficácia das mudanças implementadas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica de forma criativa o pensamento inovador e práticas de design na identificação de soluções que agregarão valor para o benefício do cliente/stakeholder.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Usa o discernimento para tomar decisões informadas sobre ações para atingir os resultados organizacionais, como o cumprimento de metas, prazos e orçamento.\nLevanta questões quando os objetivos estão em risco.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Reconhece e avalia o impacto organizacional de novas tecnologias e serviços digitais.\nImplementa práticas novas e eficazes. \nAconselha sobre os padrões, métodos, ferramentas, aplicações e processos disponíveis relevantes para a(s) especialidade(s) do grupo e pode fazer escolhas apropriadas entre alternativas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Oferece liderança em nível operacional.\nImplementa e executa políticas alinhadas aos planos estratégicos.\nAnalisa e avalia riscos.\nLeva em conta todos os requisitos ao considerar as propostas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Usa suas habilidades e conhecimentos para ajudar a estabelecer os padrões que serão aplicados por outras pessoas na organização.\nToma a iniciativa de desenvolver um conhecimento mais amplo do setor e/ou da empresa e de identificar e gerenciar oportunidades de desenvolvimento na área de responsabilidade.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Analisa, projeta, planeja, estabelece marcos, executa e avalia o trabalho de acordo com metas de tempo, custo e qualidade.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Investiga questões complexas para identificar as causas básicas e os impactos, avalia uma série de soluções e toma decisões informadas sobre o melhor curso de ação, muitas vezes em colaboração com outros especialistas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Lidera adaptações a ambientes de negócios em constante mudança.\nOrienta as equipes durante as transições, mantendo o foco nos objetivos organizacionais.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Contribui proativamente para a implementação de práticas profissionais de trabalho e ajuda a promover uma cultura organizacional de apoio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 6, + "title": "Inicia, influencia", + "guiding_phrase": "Inicia, influencia", + "essence": "Essência do nível: tem influência organizacional significativa, toma decisões de alto nível, molda políticas, demonstra liderança, promove a colaboração organizacional e aceita a responsabilidade em áreas-chave.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-6", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomia", + "description": "Orienta decisões e estratégias de alto nível dentro das políticas e dos objetivos gerais da organização. Tem autoridade e responsabilidade definidas para ações e decisões em uma área significativa de trabalho, incluindo aspectos técnicos, financeiros e de qualidade. Delega a responsabilidade pelos objetivos operacionais.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influência", + "description": "Influencia a formação da estratégia e a execução dos planos de negócios. Tem um nível gerencial significativo de contato com colegas internos e contatos externos. Exerce liderança organizacional e influência sobre a nomeação e o gerenciamento de recursos relacionados à implementação de iniciativas estratégicas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexidade", + "description": "Realiza atividades de trabalho altamente complexas que abrangem aspectos técnicos, financeiros e de qualidade.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conhecimento", + "description": "Aplica amplo conhecimento comercial para permitir a liderança estratégica e a tomada de decisões em vários domínios.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Lidera a colaboração com uma gama diversificada de partes interessadas em objetivos concorrentes dentro da organização.\nEstabelece conexões fortes e influentes com os principais contatos internos e externos em nível de gerência sênior/líder técnico", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com credibilidade em todos os níveis da organização para públicos amplos com objetivos divergentes.\nExplica informações e ideias complexas com clareza, influenciando a direção estratégica.\nPromove o compartilhamento de informações em toda a organização.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Promove iniciativas de melhoria que têm um impacto significativo na organização.\nAlinha as estratégias de aprimoramento com os objetivos organizacionais.\nEnvolve as partes interessadas nos processos de aprimoramento.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Aplica com criatividade uma ampla gama de novas ideias e técnicas de gerenciamento eficazes para obter resultados que se alinham à estratégia organizacional.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Usa o discernimento para tomar decisões que iniciam a realização dos objetivos estratégicos acordados, incluindo o desempenho financeiro.\nEscalona quando a direção estratégica mais ampla é afetada.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Lidera o aprimoramento dos recursos digitais da organização. \nIdentifica e endossa oportunidades para adotar novas tecnologias e serviços digitais.\nLidera a governança digital e a conformidade com a legislação pertinente e a necessidade de produtos e serviços.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Oferece liderança em nível organizacional.\nContribui para o desenvolvimento e a implementação de políticas e estratégias.\nCompreende e comunica os desenvolvimentos da indústria e o papel e o impacto da tecnologia. \nGerencia e mitiga o risco organizacional.  \nEquilibra os requisitos das propostas com as necessidades mais amplas da organização.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Promove a aplicação do conhecimento para apoiar os imperativos estratégicos.\nDesenvolve ativamente suas habilidades de liderança estratégica e técnica e lidera o desenvolvimento de habilidades em sua área de responsabilidade.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Inicia e influencia os objetivos estratégicos e atribui responsabilidades.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Antecipa e lidera a abordagem de problemas e oportunidades que podem afetar os objetivos organizacionais, estabelecendo uma abordagem estratégica e alocando recursos.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Promove a adaptabilidade organizacional ao iniciar e liderar mudanças significativas. Influencia as estratégias de gerenciamento de mudanças em nível organizacional.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Assume um papel de liderança na promoção e garantia de cultura e práticas de trabalho apropriadas, incluindo o fornecimento de acesso e oportunidades iguais para pessoas com habilidades diversas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + }, + { + "level": 7, + "title": "Define estrategia, inspira, mobiliza", + "guiding_phrase": "Define estratégIa, inspira, mobiliza", + "essence": "Essência do nível: Opera no mais alto nível organizacional, determina a visão e a estratégia organizacional geral e assume a responsabilidade pelo sucesso geral.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/level-7", + "generic_attributes": [ + { + "code": "AUTO", + "name": "Autonomia", + "description": "Define e lidera a visão e a estratégia da organização dentro dos objetivos comerciais mais abrangentes. É totalmente responsável pelas ações e decisões tomadas, tanto por si mesmo quanto por outros a quem foram atribuídas responsabilidades. Delega autoridade e responsabilidade pelos objetivos estratégicos do negócio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/autonomy" + }, + { + "code": "INFL", + "name": "Influência", + "description": "Dirige, influencia e inspira a direção estratégica e o desenvolvimento da organização. Tem um nível de liderança extensivo de contato com colegas internos e contatos externos. Autoriza a nomeação dos recursos necessários.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/influence" + }, + { + "code": "COMP", + "name": "Complexidade", + "description": "Exerce ampla liderança estratégica na entrega de valor comercial por meio de visão, governança e gerenciamento executivo.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/complexity" + }, + { + "code": "KNGE", + "name": "Conhecimento", + "description": "Aplica conhecimentos estratégicos e de base ampla para moldar a estratégia organizacional, prever tendências futuras do setor e preparar a organização para se adaptar e liderar.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/knowledge" + } + ], + "business_skills_behavioural_factors": [ + { + "code": "COLL", + "name": "Colaboração", + "description": "Promove a colaboração, envolvendo-se com as partes interessadas da liderança, garantindo o alinhamento com a visão e a estratégia corporativas. \nEstabelece relacionamentos sólidos e influentes com clientes, parceiros e líderes do setor.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/collaboration" + }, + { + "code": "COMM", + "name": "Comunicação", + "description": "Comunica-se com o público em todos os níveis da própria organização e se envolve com o setor.\nApresenta argumentos e ideias convincentes de forma objetiva para atingir os objetivos de negócio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/communication" + }, + { + "code": "IMPM", + "name": "Mentalidade de melhoria", + "description": "Define e comunica a abordagem organizacional para a melhoria contínua.\nCultiva uma cultura de aprimoramento contínuo.\nAvalia o impacto das iniciativas de melhoria no sucesso organizacional.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/improvement" + }, + { + "code": "CRTY", + "name": "Criatividade", + "description": "Defende a criatividade e a inovação como incentivadoras do desenvolvimento da estratégia para proporcionar oportunidades de negócio.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/creativity" + }, + { + "code": "DECM", + "name": "Tomada de Decisão", + "description": "Usa o discernimento na tomada de decisões essenciais para a direção estratégica e o sucesso da organização.\nEncaminha, quando necessário, a contribuição da gerência executiva da empresa por meio de estruturas de governança estabelecidas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/decision" + }, + { + "code": "DIGI", + "name": "Mentalidade digital", + "description": "Lidera o desenvolvimento da cultura digital da organização e a visão transformacional.  \nAumenta a capacidade e/ou a exploração da tecnologia em uma ou mais organizações por meio de um profundo entendimento do setor e das implicações das tecnologias emergentes.\nResponsabiliza-se por avaliar como as leis e os regulamentos afetam os objetivos organizacionais e seu uso de recursos digitais, de dados e de tecnologia.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/digital_mindset" + }, + { + "code": "LEAD", + "name": "Liderança", + "description": "Lidera o gerenciamento estratégico.\nAplica o mais alto nível de liderança à formulação e implementação da estratégia.\nComunica o impacto potencial das práticas emergentes e tecnologias sobre as organizações e indivíduos, e avalia os riscos de usar ou não usar tais práticas e tecnologias. \nEstabelece governança para tratar dos riscos do negócio.\nGarante que as propostas se alinhem à direção estratégica da organização.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/leadership" + }, + { + "code": "LADV", + "name": "Aprendizagem e desenvolvimento", + "description": "Inspira uma cultura de aprendizado para alinhar-se aos objetivos comerciais.   \nMantém uma visão estratégica dos cenários contemporâneos e emergentes do setor. \nGarante que a organização desenvolva e mobilize toda a gama de habilidades e capacidades necessárias.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/learning" + }, + { + "code": "PLAN", + "name": "Planejamento", + "description": "Planeja e lidera, no mais alto nível de autoridade, todos os aspectos de uma área de trabalho significativa.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/planning" + }, + { + "code": "PROB", + "name": "Resolução de Problemas", + "description": "Gerencia as inter-relações entre as partes afetadas e os imperativos estratégicos, reconhecendo o contexto comercial mais amplo e tirando conclusões precisas ao resolver problemas.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/problem" + }, + { + "code": "ADAP", + "name": "Adaptabilidade", + "description": "Promove a agilidade e a resiliência organizacional.\nIncorpora a adaptabilidade à cultura organizacional e ao planejamento estratégico.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/adaptability" + }, + { + "code": "SCPE", + "name": "Segurança, privacidade e ética", + "description": "Fornece direção clara e liderança estratégica para incorporar a conformidade, a cultura organizacional e as práticas de trabalho, além de promover ativamente a diversidade e a inclusão.", + "url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours/security" + } + ] + } + ], + "source": { + "base_url": "https://sfia-online.org/en/sfia-9/responsibilities", + "generic_attributes_url": "https://sfia-online.org/en/sfia-9/responsibilities/generic-attributes-business-skills-behaviours", + "behavioural_factors_pdf": "https://sfia-online.org/en/sfia-9/responsibilities/sfia-9-alternative-presentation-of-behavioural-factors.pdf" + } +} \ No newline at end of file diff --git a/projects/odilo/data/sfia-9-json/pt/terms-of-use.json b/projects/odilo/data/sfia-9-json/pt/terms-of-use.json new file mode 100644 index 000000000..44536a187 --- /dev/null +++ b/projects/odilo/data/sfia-9-json/pt/terms-of-use.json @@ -0,0 +1,7 @@ +{ + "type": "sfia.terms_of_use", + "sfia_version": 9, + "language": "pt", + "text": null, + "note": "Terms of use sheet not present in workbook." +} \ No newline at end of file diff --git a/reports/spec-validation-20260421.md b/reports/spec-validation-20260421.md new file mode 100644 index 000000000..3a7da05bd --- /dev/null +++ b/reports/spec-validation-20260421.md @@ -0,0 +1,72 @@ +# Specification Validation Report +**Date:** 2026-04-21 +**Validator:** Carthos (Domain Architect) +**Scope:** Terraphim Agent Session Search Specification (v1.2.0) + +--- + +## Executive Summary + +**Verdict:** **FAIL** — Critical blocker: `/sessions expand` missing + +The session-search specification is largely aligned with implementation, with strong coverage of F1–F3. However, a critical gap exists in F4.4 (Session Commands). + +--- + +## Gap Analysis + +### Gap 1: `/sessions expand` Command (BLOCKER) ❌ + +**Spec Location:** User Experience section, line 476 +**Current State:** Missing from `SessionsSubcommand` enum +**Impact:** Users cannot efficiently navigate search results with surrounding context + +**Required Implementation:** +```rust +enum SessionsSubcommand { + Expand { + target: String, // Session UUID or rank + context: Option, // Default 3 messages before/after + message_id: Option, // Optional centre message + }, + // ... rest of enum +} +``` + +**Effort:** ~300-400 lines +**Related Issue:** Gitea #703 + +--- + +### Gap 2: F5.3 Cross-Session Learning (FOLLOW-UP) ⚠️ + +**Spec Location:** Lines 449-453 +**Current State:** Session data not routed to `terraphim_agent_evolution` +**Impact:** Sessions do not contribute to agent learning or future recommendations + +**Required:** Integration with agent evolution crate after session enrichment. + +**Effort:** ~200 lines +**Related Issues:** #668, #669 + +--- + +## Feature Coverage + +| Feature | Status | Notes | +|---|---|---| +| F1: Robot Mode | ✅ PASS | All output formats, error codes, token budgets | +| F2: Forgiving CLI | ✅ PASS | Typo correction, aliases, arg flexibility | +| F3: Self-Documentation | ✅ PASS | Capabilities, schemas, examples endpoints | +| F4: Session Commands | ⚠️ PARTIAL | Missing `/sessions expand` | +| F5.1–F5.2: Knowledge Graph | ✅ PASS | Concept enrichment and discovery | +| F5.3: Cross-Session Learning | ❌ MISSING | No evolution integration | + +--- + +## Verdict + +**FAIL** — Merge blocked until Gap 1 (critical blocker) is resolved. + +Post-merge follow-up: Gap 2 (F5.3 learning integration). + diff --git a/scripts/adf-setup/migrate-to-confd.py b/scripts/adf-setup/migrate-to-confd.py new file mode 100644 index 000000000..88b96f708 --- /dev/null +++ b/scripts/adf-setup/migrate-to-confd.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "tomli-w>=1.0.0", +# ] +# /// +"""migrate-to-confd.py -- Split monolithic orchestrator TOML files into a +base config + per-project conf.d/ files. + +Usage: + uv run migrate-to-confd.py \\ + --input orchestrator.toml \\ + --input odilo-orchestrator.toml \\ + --output-dir conf.d/ \\ + --base-output orchestrator.toml + +The script is idempotent: running it twice produces byte-identical output. + +Banned model prefixes (exits non-zero on violation): + opencode/ github-copilot/ google/ huggingface/ + +Allowed providers: + kimi-for-coding/ minimax-coding-plan/ zai-coding-plan/ + opencode-go/ anthropic/ bare sonnet/opus/haiku + /path/to/claude /path/to/opencode (absolute paths) +""" + +import argparse +import sys +import re +from pathlib import Path + +# Python 3.11+ has tomllib in stdlib +try: + import tomllib +except ImportError: + try: + import tomli as tomllib # type: ignore[no-redef] + except ImportError: + print("ERROR: tomllib not available. Use Python 3.11+ or install tomli.", file=sys.stderr) + sys.exit(1) + +import tomli_w + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +BANNED_PREFIXES = [ + "opencode/", + "github-copilot/", + "google/", + "huggingface/", + "minimax/", +] + +# Global keys kept in base orchestrator.toml (not per-project). +BASE_GLOBAL_KEYS = { + "working_dir", + "restart_cooldown_secs", + "max_restart_count", + "restart_budget_window_secs", + "disk_usage_threshold", + "tick_interval_secs", + "handoff_buffer_ttl_secs", + "persona_data_dir", + "skill_data_dir", + "flow_state_dir", + "role_config_path", + "nightwatch", + "compound_review", + "routing", + "webhook", +} + +# Per-project keys that belong in [[projects]] entries. +PROJECT_LEVEL_KEYS = { + "working_dir", + "gitea", + "quickwit", + "workflow", + "mentions", +} + + +# --------------------------------------------------------------------------- +# Filename -> project_id mapping +# --------------------------------------------------------------------------- + +def project_id_from_path(path: Path) -> str: + """Derive project_id from the input filename stem. + + Rules: + orchestrator.toml -> "terraphim" + odilo-orchestrator.toml -> "odilo" + digital-twins-orchestrator.toml -> "digital-twins" + -orchestrator.toml -> "" + .toml -> "" + """ + stem = path.stem # e.g. "odilo-orchestrator" or "orchestrator" + if stem == "orchestrator": + return "terraphim" + # Strip "-orchestrator" suffix if present. + suffix = "-orchestrator" + if stem.endswith(suffix): + return stem[: -len(suffix)] + return stem + + +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- + +def _is_banned(model_value: str) -> bool: + """Return True if the model value starts with a banned prefix.""" + for prefix in BANNED_PREFIXES: + if model_value.startswith(prefix): + return True + return False + + +def validate_models(data: dict, source_path: Path) -> None: + """Validate model/fallback_model fields across all agents. + + Exits non-zero with a clear error message if a banned provider is found. + """ + agents = data.get("agents", []) + for agent in agents: + agent_name = agent.get("name", "") + for field in ("model", "fallback_model"): + value = agent.get(field) + if value and _is_banned(value): + print( + f"ERROR: Agent '{agent_name}' in {source_path} uses banned provider" + f" '{value}' (field: {field}).", + file=sys.stderr, + ) + sys.exit(1) + + # Also check compound_review.model and compound_review.fallback_model + cr = data.get("compound_review", {}) + for field in ("model", "fallback_model"): + value = cr.get(field) if cr else None + if value and _is_banned(value): + print( + f"ERROR: compound_review in {source_path} uses banned provider" + f" '{value}' (field: {field}).", + file=sys.stderr, + ) + sys.exit(1) + + +# --------------------------------------------------------------------------- +# Transformation helpers +# --------------------------------------------------------------------------- + +def _deep_copy(obj): + """Return a plain Python deep copy without external deps.""" + import copy + return copy.deepcopy(obj) + + +def build_project_entry(data: dict, project_id: str) -> dict: + """Build a [[projects]] dict from top-level fields of a monolithic config.""" + entry: dict = {"id": project_id} + # working_dir is required on Project + if "working_dir" in data: + entry["working_dir"] = str(data["working_dir"]) + for key in ("gitea", "quickwit", "workflow", "mentions"): + if key in data: + entry[key] = _deep_copy(data[key]) + return entry + + +def build_agent_entries(data: dict, project_id: str) -> list: + """Return agents list with project field injected.""" + agents = [] + for agent in data.get("agents", []): + a = _deep_copy(agent) + # Only set project if not already set + if "project" not in a: + a["project"] = project_id + agents.append(a) + return agents + + +def build_flow_entries(data: dict, project_id: str) -> list: + """Return flows list with project field injected.""" + flows = [] + for flow in data.get("flows", []): + f = _deep_copy(flow) + if "project" not in f: + f["project"] = project_id + flows.append(f) + return flows + + +def build_confd_doc(data: dict, project_id: str) -> dict: + """Build the per-project conf.d TOML document.""" + doc: dict = {} + + project_entry = build_project_entry(data, project_id) + doc["projects"] = [project_entry] + + agents = build_agent_entries(data, project_id) + if agents: + doc["agents"] = agents + + flows = build_flow_entries(data, project_id) + if flows: + doc["flows"] = flows + + return doc + + +def build_base_doc(inputs: list[tuple[Path, dict]], include_glob: str) -> dict: + """Build the base orchestrator.toml document. + + Global settings are taken from the first input that defines them. + include glob is set to the provided pattern. + Keys are emitted in a deterministic order so that repeated runs produce + byte-identical output. + """ + # Collect global settings from inputs (first wins), using sorted key order + # to ensure deterministic serialisation. + base: dict = {} + + for _path, data in inputs: + for key in sorted(BASE_GLOBAL_KEYS): + if key not in base and key in data: + base[key] = _deep_copy(data[key]) + + # Remove per-project keys that leaked into globals (e.g. working_dir is + # also a Project field, but OrchestratorConfig.working_dir is the global). + # We keep working_dir in base because OrchestratorConfig has it. + + # Set include glob. + base["include"] = [include_glob] + + # Ensure required nightwatch / compound_review placeholders exist with + # sensible defaults if missing from all inputs. + if "nightwatch" not in base: + base["nightwatch"] = { + "eval_interval_secs": 300, + "minor_threshold": 0.10, + "moderate_threshold": 0.20, + "severe_threshold": 0.40, + "critical_threshold": 0.70, + } + if "compound_review" not in base: + base["compound_review"] = { + "schedule": "0 2 * * *", + "repo_path": ".", + } + + return base + + +# --------------------------------------------------------------------------- +# TOML serialisation helper +# --------------------------------------------------------------------------- + +def _normalise(obj): + """Recursively normalise types for tomli_w serialisation. + + tomli_w only accepts: str, int, float, bool, dict, list, datetime, Path + is not accepted -- convert to str. + """ + if isinstance(obj, dict): + return {k: _normalise(v) for k, v in obj.items()} + if isinstance(obj, list): + return [_normalise(v) for v in obj] + if isinstance(obj, Path): + return str(obj) + return obj + + +def serialise_toml(doc: dict) -> bytes: + """Serialise a dict to TOML bytes using tomli_w.""" + return tomli_w.dumps(_normalise(doc)).encode("utf-8") + + +# --------------------------------------------------------------------------- +# Main logic +# --------------------------------------------------------------------------- + +def run( + input_paths: list[Path], + output_dir: Path, + base_output: Path, + dry_run: bool, +) -> None: + # Parse and validate all inputs first. + inputs: list[tuple[Path, dict]] = [] + for path in input_paths: + if not path.exists(): + print(f"ERROR: Input file not found: {path}", file=sys.stderr) + sys.exit(1) + with open(path, "rb") as fh: + data = tomllib.load(fh) + validate_models(data, path) + inputs.append((path, data)) + + # Build per-project conf.d files. + confd_files: list[tuple[Path, bytes]] = [] + for path, data in inputs: + pid = project_id_from_path(path) + confd_doc = build_confd_doc(data, pid) + confd_bytes = serialise_toml(confd_doc) + out_path = output_dir / f"{pid}.toml" + confd_files.append((out_path, confd_bytes)) + + # Determine include glob relative to base_output parent. + # We emit a simple relative glob "conf.d/*.toml". + include_glob = "conf.d/*.toml" + + # Build base orchestrator.toml. + base_doc = build_base_doc(inputs, include_glob) + base_bytes = serialise_toml(base_doc) + + # Report what would be written. + if dry_run: + print(f"[dry-run] Would write base config: {base_output}") + for out_path, _ in confd_files: + print(f"[dry-run] Would write conf.d file: {out_path}") + print("[dry-run] No files written.") + return + + # Write output. + output_dir.mkdir(parents=True, exist_ok=True) + base_output.parent.mkdir(parents=True, exist_ok=True) + + base_output.write_bytes(base_bytes) + print(f"Written: {base_output}") + + for out_path, content in confd_files: + out_path.write_bytes(content) + print(f"Written: {out_path}") + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="Migrate monolithic orchestrator TOML files to conf.d/ layout.", + ) + parser.add_argument( + "--input", + dest="inputs", + action="append", + required=True, + metavar="PATH", + help="Input TOML file (repeatable).", + ) + parser.add_argument( + "--output-dir", + default="scripts/adf-setup/conf.d/", + metavar="DIR", + help="Output directory for per-project conf.d/ files (default: scripts/adf-setup/conf.d/).", + ) + parser.add_argument( + "--base-output", + default="scripts/adf-setup/orchestrator.toml", + metavar="PATH", + help="Output path for the base orchestrator.toml (default: scripts/adf-setup/orchestrator.toml).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print what would be written without actually writing files.", + ) + args = parser.parse_args() + + input_paths = [Path(p) for p in args.inputs] + output_dir = Path(args.output_dir) + base_output = Path(args.base_output) + + run(input_paths, output_dir, base_output, args.dry_run) + + +if __name__ == "__main__": + main() diff --git a/scripts/adf-setup/scripts/adf-setup/orchestrator.toml b/scripts/adf-setup/scripts/adf-setup/orchestrator.toml new file mode 100644 index 000000000..b21efa4ae --- /dev/null +++ b/scripts/adf-setup/scripts/adf-setup/orchestrator.toml @@ -0,0 +1,42 @@ +disk_usage_threshold = 90 +flow_state_dir = "/opt/ai-dark-factory/flow-states" +max_restart_count = 3 +persona_data_dir = "/home/alex/terraphim-ai/data/personas" +restart_cooldown_secs = 300 +role_config_path = "/opt/ai-dark-factory/persona_roles_config.json" +skill_data_dir = "/opt/ai-dark-factory/skills" +tick_interval_secs = 30 +working_dir = "/home/alex/terraphim-ai" +include = [ + "conf.d/*.toml", +] + +[compound_review] +schedule = "0 0-10 * * *" +max_duration_secs = 1800 +repo_path = "/home/alex/terraphim-ai" +create_prs = false +worktree_root = "/home/alex/terraphim-ai/.worktrees" +cli_tool = "/home/alex/.bun/bin/opencode" +gitea_issue = 514 +auto_file_issues = false +auto_remediate = false + +[nightwatch] +eval_interval_secs = 300 +active_start_hour = 2 +active_end_hour = 6 +minor_threshold = 0.1 +moderate_threshold = 0.2 +severe_threshold = 0.4 +critical_threshold = 0.7 + +[routing] +taxonomy_path = "/home/alex/terraphim-ai/docs/taxonomy/routing_scenarios/adf" +probe_ttl_secs = 300 +probe_results_dir = "/home/alex/.terraphim/benchmark-results" +probe_on_startup = true + +[webhook] +bind = "172.18.0.1:9091" +secret = "85af336ebec99fe512871106650bce179f9a2b218f40576f76d5ff0a55e7af9a" diff --git a/scripts/adf-setup/tests/expected/odilo.toml b/scripts/adf-setup/tests/expected/odilo.toml new file mode 100644 index 000000000..bbe3048ca --- /dev/null +++ b/scripts/adf-setup/tests/expected/odilo.toml @@ -0,0 +1,49 @@ +[[projects]] +id = "odilo" +working_dir = "/home/alex/projects/odilo" + +[projects.gitea] +base_url = "https://git.example.test" +token = "fixture-token-not-real" +owner = "example-org" +repo = "odilo" + +[projects.quickwit] +enabled = true +endpoint = "http://127.0.0.1:7280" +index_id = "adf-odilo-logs" +batch_size = 100 +flush_interval_secs = 5 + +[[agents]] +name = "odilo-developer" +layer = "Core" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +persona = "Lux" +terraphim_role = "Rust Engineer" +skill_chain = [ + "disciplined-research", + "disciplined-implementation", +] +schedule = "0 1-9 * * *" +max_cpu_seconds = 1800 +grace_period_secs = 30 +task = "Pick the highest PageRank unblocked issue and implement it." +project = "odilo" + +[[agents]] +name = "odilo-reviewer" +layer = "Core" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +fallback_model = "kimi-for-coding/k2p5" +persona = "Lux" +skill_chain = [ + "disciplined-verification", +] +schedule = "0 2-10 * * *" +max_cpu_seconds = 900 +grace_period_secs = 30 +task = "Review open PRs and post verdict." +project = "odilo" diff --git a/scripts/adf-setup/tests/expected/orchestrator.toml b/scripts/adf-setup/tests/expected/orchestrator.toml new file mode 100644 index 000000000..380de7eef --- /dev/null +++ b/scripts/adf-setup/tests/expected/orchestrator.toml @@ -0,0 +1,28 @@ +disk_usage_threshold = 90 +flow_state_dir = "/opt/ai-dark-factory/flow-states" +max_restart_count = 3 +persona_data_dir = "/home/alex/terraphim-ai/data/personas" +restart_cooldown_secs = 300 +role_config_path = "/opt/ai-dark-factory/persona_roles_config.json" +skill_data_dir = "/opt/ai-dark-factory/skills" +tick_interval_secs = 30 +working_dir = "/home/alex/terraphim-ai" +include = [ + "conf.d/*.toml", +] + +[compound_review] +schedule = "0 0-10 * * *" +max_duration_secs = 1800 +repo_path = "/home/alex/terraphim-ai" +create_prs = false +worktree_root = "/home/alex/terraphim-ai/.worktrees" + +[nightwatch] +eval_interval_secs = 300 +active_start_hour = 2 +active_end_hour = 6 +minor_threshold = 0.1 +moderate_threshold = 0.2 +severe_threshold = 0.4 +critical_threshold = 0.7 diff --git a/scripts/adf-setup/tests/expected/terraphim.toml b/scripts/adf-setup/tests/expected/terraphim.toml new file mode 100644 index 000000000..9b326f8e9 --- /dev/null +++ b/scripts/adf-setup/tests/expected/terraphim.toml @@ -0,0 +1,83 @@ +[[projects]] +id = "terraphim" +working_dir = "/home/alex/terraphim-ai" + +[projects.gitea] +base_url = "https://git.example.test" +token = "fixture-token-not-real" +owner = "terraphim" +repo = "terraphim-ai" + +[projects.quickwit] +enabled = true +endpoint = "http://127.0.0.1:7280" +index_id = "adf-logs" +batch_size = 100 +flush_interval_secs = 5 + +[projects.workflow] +enabled = true +workflow_file = "/home/alex/terraphim-ai/WORKFLOW.md" + +[projects.workflow.tracker] +kind = "gitea" +endpoint = "https://git.example.test" +api_key = "fixture-token-not-real" +owner = "terraphim" +repo = "terraphim-ai" + +[[agents]] +name = "security-sentinel" +layer = "Safety" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +fallback_model = "kimi-for-coding/k2p5" +persona = "Vigil" +skill_chain = [ + "disciplined-verification", +] +max_cpu_seconds = 600 +grace_period_secs = 30 +task = "Run security audit on the terraphim-ai project." +project = "terraphim" + +[[agents]] +name = "compliance-auditor" +layer = "Core" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +fallback_model = "opencode-go/minimax-m2.5" +persona = "Audit" +skill_chain = [ + "disciplined-validation", +] +schedule = "0 2 * * *" +max_cpu_seconds = 900 +grace_period_secs = 30 +task = "Run compliance checks." +project = "terraphim" + +[[agents]] +name = "quality-coordinator" +layer = "Growth" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +persona = "Conduit" +skill_chain = [ + "disciplined-design", +] +max_cpu_seconds = 300 +grace_period_secs = 30 +task = "Coordinate quality review chain." +project = "terraphim" + +[[flows]] +name = "security-audit-flow" +project = "terraphim" +repo_path = "/home/alex/terraphim-ai" +base_branch = "main" +timeout_secs = 3600 +schedule = "0 */6 * * *" +steps = [ + { name = "run-cargo-audit", kind = "action", command = "cargo audit", timeout_secs = 300 }, +] diff --git a/scripts/adf-setup/tests/fixtures/banned-provider.toml b/scripts/adf-setup/tests/fixtures/banned-provider.toml new file mode 100644 index 000000000..869c8ba0e --- /dev/null +++ b/scripts/adf-setup/tests/fixtures/banned-provider.toml @@ -0,0 +1,23 @@ +# Fixture with a banned provider -- migration must exit non-zero. +working_dir = "/home/alex/projects/example" +restart_cooldown_secs = 300 +max_restart_count = 3 +tick_interval_secs = 30 + +[nightwatch] +eval_interval_secs = 300 +minor_threshold = 0.10 +moderate_threshold = 0.20 +severe_threshold = 0.40 +critical_threshold = 0.70 + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/home/alex/projects/example" + +[[agents]] +name = "bad-agent" +layer = "Core" +cli_tool = "/home/alex/.bun/bin/opencode" +model = "opencode/gpt-4o" +task = "Do something." diff --git a/scripts/adf-setup/tests/fixtures/odilo-orchestrator.toml b/scripts/adf-setup/tests/fixtures/odilo-orchestrator.toml new file mode 100644 index 000000000..ff9693f02 --- /dev/null +++ b/scripts/adf-setup/tests/fixtures/odilo-orchestrator.toml @@ -0,0 +1,62 @@ +# Synthetic odilo project fixture (no real tokens or private data) +working_dir = "/home/alex/projects/odilo" +restart_cooldown_secs = 300 +max_restart_count = 3 +disk_usage_threshold = 90 +tick_interval_secs = 60 +persona_data_dir = "/home/alex/terraphim-ai/data/personas" + +[quickwit] +enabled = true +endpoint = "http://127.0.0.1:7280" +index_id = "adf-odilo-logs" +batch_size = 100 +flush_interval_secs = 5 + +[gitea] +base_url = "https://git.example.test" +token = "fixture-token-not-real" +owner = "example-org" +repo = "odilo" + +[nightwatch] +eval_interval_secs = 600 +active_start_hour = 1 +active_end_hour = 9 +minor_threshold = 0.10 +moderate_threshold = 0.20 +severe_threshold = 0.40 +critical_threshold = 0.70 + +[compound_review] +schedule = "0 3 * * *" +max_duration_secs = 900 +repo_path = "/home/alex/projects/odilo" +create_prs = false +worktree_root = "/home/alex/projects/odilo/.worktrees" + +[[agents]] +name = "odilo-developer" +layer = "Core" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +persona = "Lux" +terraphim_role = "Rust Engineer" +skill_chain = ["disciplined-research", "disciplined-implementation"] +schedule = "0 1-9 * * *" +max_cpu_seconds = 1800 +grace_period_secs = 30 +task = "Pick the highest PageRank unblocked issue and implement it." + +[[agents]] +name = "odilo-reviewer" +layer = "Core" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +fallback_model = "kimi-for-coding/k2p5" +persona = "Lux" +skill_chain = ["disciplined-verification"] +schedule = "0 2-10 * * *" +max_cpu_seconds = 900 +grace_period_secs = 30 +task = "Review open PRs and post verdict." diff --git a/scripts/adf-setup/tests/fixtures/orchestrator.toml b/scripts/adf-setup/tests/fixtures/orchestrator.toml new file mode 100644 index 000000000..9e026f904 --- /dev/null +++ b/scripts/adf-setup/tests/fixtures/orchestrator.toml @@ -0,0 +1,100 @@ +# Synthetic terraphim project fixture (no real tokens or private data) +working_dir = "/home/alex/terraphim-ai" +restart_cooldown_secs = 300 +max_restart_count = 3 +disk_usage_threshold = 90 +tick_interval_secs = 30 +persona_data_dir = "/home/alex/terraphim-ai/data/personas" +skill_data_dir = "/opt/ai-dark-factory/skills" +flow_state_dir = "/opt/ai-dark-factory/flow-states" +role_config_path = "/opt/ai-dark-factory/persona_roles_config.json" + +[quickwit] +enabled = true +endpoint = "http://127.0.0.1:7280" +index_id = "adf-logs" +batch_size = 100 +flush_interval_secs = 5 + +[gitea] +base_url = "https://git.example.test" +token = "fixture-token-not-real" +owner = "terraphim" +repo = "terraphim-ai" + +[nightwatch] +eval_interval_secs = 300 +active_start_hour = 2 +active_end_hour = 6 +minor_threshold = 0.10 +moderate_threshold = 0.20 +severe_threshold = 0.40 +critical_threshold = 0.70 + +[compound_review] +schedule = "0 0-10 * * *" +max_duration_secs = 1800 +repo_path = "/home/alex/terraphim-ai" +create_prs = false +worktree_root = "/home/alex/terraphim-ai/.worktrees" + +[workflow] +enabled = true +workflow_file = "/home/alex/terraphim-ai/WORKFLOW.md" + +[workflow.tracker] +kind = "gitea" +endpoint = "https://git.example.test" +api_key = "fixture-token-not-real" +owner = "terraphim" +repo = "terraphim-ai" + +[[agents]] +name = "security-sentinel" +layer = "Safety" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +fallback_model = "kimi-for-coding/k2p5" +persona = "Vigil" +skill_chain = ["disciplined-verification"] +max_cpu_seconds = 600 +grace_period_secs = 30 +task = "Run security audit on the terraphim-ai project." + +[[agents]] +name = "compliance-auditor" +layer = "Core" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +fallback_model = "opencode-go/minimax-m2.5" +persona = "Audit" +skill_chain = ["disciplined-validation"] +schedule = "0 2 * * *" +max_cpu_seconds = 900 +grace_period_secs = 30 +task = "Run compliance checks." + +[[agents]] +name = "quality-coordinator" +layer = "Growth" +cli_tool = "/home/alex/.local/bin/claude" +model = "sonnet" +persona = "Conduit" +skill_chain = ["disciplined-design"] +max_cpu_seconds = 300 +grace_period_secs = 30 +task = "Coordinate quality review chain." + +[[flows]] +name = "security-audit-flow" +project = "terraphim" +repo_path = "/home/alex/terraphim-ai" +base_branch = "main" +timeout_secs = 3600 +schedule = "0 */6 * * *" + +[[flows.steps]] +name = "run-cargo-audit" +kind = "action" +command = "cargo audit" +timeout_secs = 300 diff --git a/scripts/adf-setup/tests/test_migrate.py b/scripts/adf-setup/tests/test_migrate.py new file mode 100644 index 000000000..48dfa4a07 --- /dev/null +++ b/scripts/adf-setup/tests/test_migrate.py @@ -0,0 +1,457 @@ +"""Tests for migrate-to-confd.py + +Treats the script as a black box -- invoked via subprocess. +No mocks used. +""" + +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +import pytest + +# Absolute path to the script under test. +SCRIPT = Path(__file__).parent.parent / "migrate-to-confd.py" +FIXTURES = Path(__file__).parent / "fixtures" + +# Rust orchestrator config source (relative to workspace root). +WORKSPACE_ROOT = Path(__file__).parent.parent.parent.parent +RUST_CONFIG_SRC = WORKSPACE_ROOT / "crates/terraphim_orchestrator/src/config.rs" + + +def run_migration(*extra_args, check=False, cwd=None): + """Invoke migrate-to-confd.py via uv run and return CompletedProcess.""" + cmd = ["uv", "run", str(SCRIPT)] + list(extra_args) + return subprocess.run( + cmd, + capture_output=True, + text=True, + check=check, + cwd=cwd, + ) + + +# --------------------------------------------------------------------------- +# Test 1: Round-trip -- fixture input produces expected output structure +# --------------------------------------------------------------------------- + +def test_round_trip_structure(): + """Running the migration on fixtures produces correct [[projects]], [[agents]], [[flows]].""" + try: + import tomllib + except ImportError: + import tomli as tomllib # type: ignore[no-redef] + + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + confd_dir = tmp_path / "conf.d" + base_out = tmp_path / "orchestrator.toml" + + result = run_migration( + "--input", str(FIXTURES / "orchestrator.toml"), + "--input", str(FIXTURES / "odilo-orchestrator.toml"), + "--output-dir", str(confd_dir), + "--base-output", str(base_out), + ) + assert result.returncode == 0, f"Script failed:\n{result.stderr}" + + # --- base orchestrator.toml checks --- + with open(base_out, "rb") as fh: + base = tomllib.load(fh) + + assert "include" in base, "base must have 'include' key" + assert base["include"] == ["conf.d/*.toml"], f"unexpected include: {base['include']}" + assert "agents" not in base, "base must not contain agents" + assert "flows" not in base, "base must not contain flows" + assert "projects" not in base, "base must not contain projects" + assert "working_dir" in base, "base must have working_dir" + assert "nightwatch" in base, "base must have nightwatch" + assert "compound_review" in base, "base must have compound_review" + + # --- terraphim.toml checks --- + terraphim_path = confd_dir / "terraphim.toml" + assert terraphim_path.exists(), "terraphim.toml not created" + with open(terraphim_path, "rb") as fh: + terraphim = tomllib.load(fh) + + projects = terraphim.get("projects", []) + assert len(projects) == 1, f"expected 1 project, got {len(projects)}" + assert projects[0]["id"] == "terraphim" + assert projects[0]["working_dir"] == "/home/alex/terraphim-ai" + + agents = terraphim.get("agents", []) + assert len(agents) == 3, f"expected 3 agents, got {len(agents)}" + for agent in agents: + assert agent.get("project") == "terraphim", ( + f"agent '{agent['name']}' missing project='terraphim'" + ) + + flows = terraphim.get("flows", []) + assert len(flows) == 1, f"expected 1 flow, got {len(flows)}" + assert flows[0]["project"] == "terraphim" + assert flows[0]["name"] == "security-audit-flow" + + # --- odilo.toml checks --- + odilo_path = confd_dir / "odilo.toml" + assert odilo_path.exists(), "odilo.toml not created" + with open(odilo_path, "rb") as fh: + odilo = tomllib.load(fh) + + o_projects = odilo.get("projects", []) + assert len(o_projects) == 1 + assert o_projects[0]["id"] == "odilo" + assert o_projects[0]["working_dir"] == "/home/alex/projects/odilo" + + o_agents = odilo.get("agents", []) + assert len(o_agents) == 2 + for agent in o_agents: + assert agent.get("project") == "odilo", ( + f"odilo agent '{agent['name']}' missing project='odilo'" + ) + + # odilo has no flows -- key should be absent or empty + assert odilo.get("flows", []) == [] + + +# --------------------------------------------------------------------------- +# Test 2: Idempotence -- running twice produces byte-identical output +# --------------------------------------------------------------------------- + +def test_idempotent(): + """Running the migration twice produces byte-identical output files.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + + def run_once(run_id: int): + confd_dir = tmp_path / f"run{run_id}" / "conf.d" + base_out = tmp_path / f"run{run_id}" / "orchestrator.toml" + result = run_migration( + "--input", str(FIXTURES / "orchestrator.toml"), + "--input", str(FIXTURES / "odilo-orchestrator.toml"), + "--output-dir", str(confd_dir), + "--base-output", str(base_out), + ) + assert result.returncode == 0, f"Run {run_id} failed:\n{result.stderr}" + return base_out, confd_dir + + base1, confd1 = run_once(1) + base2, confd2 = run_once(2) + + # Compare base files. + assert base1.read_bytes() == base2.read_bytes(), "base orchestrator.toml differs between runs" + + # Compare each conf.d file. + for name in ["terraphim.toml", "odilo.toml"]: + b1 = (confd1 / name).read_bytes() + b2 = (confd2 / name).read_bytes() + assert b1 == b2, f"conf.d/{name} differs between runs" + + +# --------------------------------------------------------------------------- +# Test 3: C1 rejection -- banned-provider input exits non-zero with agent name +# --------------------------------------------------------------------------- + +def test_banned_provider_rejected(): + """Script must exit non-zero when an agent uses a banned provider prefix.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + result = run_migration( + "--input", str(FIXTURES / "banned-provider.toml"), + "--output-dir", str(tmp_path / "conf.d"), + "--base-output", str(tmp_path / "orchestrator.toml"), + ) + assert result.returncode != 0, "Expected non-zero exit for banned provider" + assert "banned" in result.stderr.lower() or "ERROR" in result.stderr, ( + f"Expected error message in stderr, got:\n{result.stderr}" + ) + assert "bad-agent" in result.stderr, ( + f"Expected agent name 'bad-agent' in error, got:\n{result.stderr}" + ) + assert "opencode/" in result.stderr, ( + f"Expected banned value 'opencode/' in error, got:\n{result.stderr}" + ) + + +# --------------------------------------------------------------------------- +# Test 4: Flow project injection -- flows get project field added +# --------------------------------------------------------------------------- + +def test_flow_project_injection(): + """Each flow in the output has the correct project field.""" + try: + import tomllib + except ImportError: + import tomli as tomllib # type: ignore[no-redef] + + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + confd_dir = tmp_path / "conf.d" + base_out = tmp_path / "orchestrator.toml" + + result = run_migration( + "--input", str(FIXTURES / "orchestrator.toml"), + "--output-dir", str(confd_dir), + "--base-output", str(base_out), + ) + assert result.returncode == 0, f"Script failed:\n{result.stderr}" + + terraphim_path = confd_dir / "terraphim.toml" + with open(terraphim_path, "rb") as fh: + doc = tomllib.load(fh) + + flows = doc.get("flows", []) + assert len(flows) >= 1, "Expected at least one flow in terraphim.toml" + for flow in flows: + assert "project" in flow, f"Flow '{flow.get('name')}' missing project field" + assert flow["project"] == "terraphim", ( + f"Flow '{flow.get('name')}' has wrong project: {flow['project']!r}" + ) + + +# --------------------------------------------------------------------------- +# Test 5: Dry-run -- no files written +# --------------------------------------------------------------------------- + +def test_dry_run_writes_nothing(): + """With --dry-run, no output files are created.""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + confd_dir = tmp_path / "conf.d" + base_out = tmp_path / "orchestrator.toml" + + result = run_migration( + "--dry-run", + "--input", str(FIXTURES / "orchestrator.toml"), + "--output-dir", str(confd_dir), + "--base-output", str(base_out), + ) + assert result.returncode == 0, f"Script failed:\n{result.stderr}" + assert not base_out.exists(), "base file should not be written in dry-run" + assert not confd_dir.exists(), "conf.d dir should not be created in dry-run" + assert "dry-run" in result.stdout.lower(), "Expected dry-run notice in stdout" + + +# --------------------------------------------------------------------------- +# Test 6: github-copilot/ prefix also rejected +# --------------------------------------------------------------------------- + +def test_github_copilot_banned(): + """github-copilot/ prefix is also a banned provider.""" + # Write a minimal inline TOML fixture as a plain string -- no need for + # external serialisation library in the test itself. + fixture_toml = """\ +working_dir = "/tmp/test" +restart_cooldown_secs = 300 +max_restart_count = 3 +tick_interval_secs = 30 + +[nightwatch] +eval_interval_secs = 300 +minor_threshold = 0.1 +moderate_threshold = 0.2 +severe_threshold = 0.4 +critical_threshold = 0.7 + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/test" + +[[agents]] +name = "copilot-agent" +layer = "Core" +cli_tool = "/usr/bin/gh" +model = "github-copilot/gpt-4o" +task = "Do something." +""" + + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + fixture_path = tmp_path / "copilot-orchestrator.toml" + fixture_path.write_text(fixture_toml, encoding="utf-8") + + result = run_migration( + "--input", str(fixture_path), + "--output-dir", str(tmp_path / "conf.d"), + "--base-output", str(tmp_path / "orchestrator.toml"), + ) + assert result.returncode != 0, "Expected non-zero exit for github-copilot provider" + assert "copilot-agent" in result.stderr + assert "github-copilot/" in result.stderr + + +# --------------------------------------------------------------------------- +# Helper: find or build the adf binary +# --------------------------------------------------------------------------- + +def _find_adf_binary() -> Path | None: + """Return path to the adf binary, or None if it cannot be located or built.""" + # 1. Set by cargo when running from the Rust test harness. + env_path = os.environ.get("CARGO_BIN_EXE_adf") + if env_path and Path(env_path).is_file(): + return Path(env_path) + + # 2. CARGO_TARGET_DIR override. + cargo_target = os.environ.get("CARGO_TARGET_DIR") + if cargo_target: + candidate = Path(cargo_target) / "debug" / "adf" + if candidate.is_file(): + return candidate + + # 3. Workspace-relative default target dir. + for profile in ("debug", "release"): + candidate = WORKSPACE_ROOT / "target" / profile / "adf" + if candidate.is_file(): + return candidate + + # 4. Try to build. + build_env = dict(os.environ) + target_dir = os.environ.get("CARGO_TARGET_DIR", str(WORKSPACE_ROOT / "target")) + result = subprocess.run( + ["cargo", "build", "--bin", "adf"], + cwd=str(WORKSPACE_ROOT), + capture_output=True, + env={**build_env, "CARGO_TARGET_DIR": target_dir}, + ) + if result.returncode == 0: + candidate = Path(target_dir) / "debug" / "adf" + if candidate.is_file(): + return candidate + + return None + + +# --------------------------------------------------------------------------- +# Test 7: banned-list drift detection -- script list must match Rust source +# --------------------------------------------------------------------------- + +def test_banned_list_matches_rust(): + """Script BANNED_PREFIXES must match BANNED_PROVIDER_PREFIXES in Rust config.rs. + + Parses the Rust source to extract the constant and compares with the + Python script's list (normalising the trailing '/' convention). + """ + assert RUST_CONFIG_SRC.exists(), ( + f"Rust config source not found: {RUST_CONFIG_SRC}" + ) + + rust_src = RUST_CONFIG_SRC.read_text(encoding="utf-8") + + # Extract the BANNED_PROVIDER_PREFIXES constant block. + match = re.search( + r'pub const BANNED_PROVIDER_PREFIXES:\s*&\[&str\]\s*=\s*&\[([^\]]*)\]', + rust_src, + re.DOTALL, + ) + assert match, "Could not find BANNED_PROVIDER_PREFIXES in Rust config.rs" + + rust_entries = re.findall(r'"([^"]+)"', match.group(1)) + rust_set = set(rust_entries) + + # Import the script's list without executing it fully -- extract via regex. + script_src = SCRIPT.read_text(encoding="utf-8") + script_match = re.search( + r'BANNED_PREFIXES\s*=\s*\[([^\]]*)\]', + script_src, + re.DOTALL, + ) + assert script_match, "Could not find BANNED_PREFIXES in migrate-to-confd.py" + + script_entries = re.findall(r'"([^"]+)"', script_match.group(1)) + # Normalise: strip trailing '/' so both sets use bare prefix names. + script_set = {e.rstrip("/") for e in script_entries} + + assert script_set == rust_set, ( + f"BANNED_PREFIXES mismatch.\n" + f" Script (normalised): {sorted(script_set)}\n" + f" Rust: {sorted(rust_set)}\n" + f" Missing from script: {sorted(rust_set - script_set)}\n" + f" Extra in script: {sorted(script_set - rust_set)}" + ) + + +# --------------------------------------------------------------------------- +# Test 8: minimax/ bare prefix is banned (regression for P1-1 sync) +# --------------------------------------------------------------------------- + +def test_minimax_bare_prefix_rejected(): + """minimax/ prefix must be banned, matching the Rust validator.""" + fixture_toml = """\ +working_dir = "/tmp/test" +restart_cooldown_secs = 300 +max_restart_count = 3 +tick_interval_secs = 30 + +[nightwatch] +eval_interval_secs = 300 +minor_threshold = 0.1 +moderate_threshold = 0.2 +severe_threshold = 0.4 +critical_threshold = 0.7 + +[compound_review] +schedule = "0 2 * * *" +repo_path = "/tmp/test" + +[[agents]] +name = "minimax-agent" +layer = "Core" +cli_tool = "/usr/bin/opencode" +model = "minimax/abab-7" +task = "Do something." +""" + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + fixture_path = tmp_path / "minimax-orchestrator.toml" + fixture_path.write_text(fixture_toml, encoding="utf-8") + + result = run_migration( + "--input", str(fixture_path), + "--output-dir", str(tmp_path / "conf.d"), + "--base-output", str(tmp_path / "orchestrator.toml"), + ) + assert result.returncode != 0, ( + "Expected non-zero exit for minimax/ provider (bare, not minimax-coding-plan/)" + ) + assert "minimax-agent" in result.stderr, ( + f"Expected agent name in error: {result.stderr}" + ) + assert "minimax/" in result.stderr, ( + f"Expected banned value in error: {result.stderr}" + ) + + +# --------------------------------------------------------------------------- +# Test 9: adf --check accepts generated output (P1-3) +# --------------------------------------------------------------------------- + +def test_adf_check_accepts_generated_output(): + """adf --check must exit 0 on a complete generated conf.d layout.""" + adf = _find_adf_binary() + if adf is None: + pytest.skip("adf binary not available and cargo build failed -- skipping") + + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + confd_dir = tmp_path / "conf.d" + base_out = tmp_path / "orchestrator.toml" + + result = run_migration( + "--input", str(FIXTURES / "orchestrator.toml"), + "--input", str(FIXTURES / "odilo-orchestrator.toml"), + "--output-dir", str(confd_dir), + "--base-output", str(base_out), + ) + assert result.returncode == 0, f"Migration failed:\n{result.stderr}" + + check = subprocess.run( + [str(adf), "--check", str(base_out)], + capture_output=True, + text=True, + ) + assert check.returncode == 0, ( + f"adf --check failed with exit {check.returncode}.\n" + f"stdout: {check.stdout}\nstderr: {check.stderr}" + ) diff --git a/scripts/setup_system_operator.sh b/scripts/setup_system_operator.sh index 59a946111..86d41dae4 100755 --- a/scripts/setup_system_operator.sh +++ b/scripts/setup_system_operator.sh @@ -1,72 +1,58 @@ -#!/bin/bash - -# Setup script for Terraphim System Operator with remote knowledge graph -# This script will: -# 1. Clone the system-operator repository -# 2. Set up the server configuration -# 3. Start the server with the system operator configuration - -set -e - -echo "🚀 Setting up Terraphim System Operator with remote knowledge graph..." - -# Configuration -SYSTEM_OPERATOR_DIR="${SYSTEM_OPERATOR_DIR:-/tmp/system_operator}" +#!/usr/bin/env bash +# Setup script for the Terraphim System Operator demo. +# +# Clones the terraphim/system-operator Logseq vault into a durable path +# under ~/.config/terraphim/system_operator, counts the pages and synonym +# files, and prints the commands to drive it via terraphim_server and the +# terraphim-agent CLI. +# +# Override the target path with SYSTEM_OPERATOR_DIR= if you want it +# elsewhere; the previous /tmp default lost the clone on reboot. + +set -euo pipefail + +SYSTEM_OPERATOR_DIR="${SYSTEM_OPERATOR_DIR:-$HOME/.config/terraphim/system_operator}" CONFIG_FILE="terraphim_server/default/system_operator_config.json" SERVER_SETTINGS="terraphim_server/default/settings_system_operator_server.toml" -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo -e "${BLUE}📁 Setting up system operator directory...${NC}" +echo "[setup] Target directory: ${SYSTEM_OPERATOR_DIR}" -# Create directory if it doesn't exist -mkdir -p "$SYSTEM_OPERATOR_DIR" +mkdir -p "${SYSTEM_OPERATOR_DIR}" -# Clone or update the repository -if [ -d "$SYSTEM_OPERATOR_DIR/.git" ]; then - echo -e "${YELLOW}📦 Repository already exists, updating...${NC}" - cd "$SYSTEM_OPERATOR_DIR" - git pull origin main +if [ -d "${SYSTEM_OPERATOR_DIR}/.git" ]; then + echo "[setup] Repository exists, updating..." + git -C "${SYSTEM_OPERATOR_DIR}" pull --ff-only origin main else - echo -e "${YELLOW}📦 Cloning system-operator repository...${NC}" - git clone https://github.com/terraphim/system-operator.git "$SYSTEM_OPERATOR_DIR" + echo "[setup] Cloning system-operator repository..." + git clone https://github.com/terraphim/system-operator.git "${SYSTEM_OPERATOR_DIR}" fi -# Check if repository was cloned successfully -if [ ! -d "$SYSTEM_OPERATOR_DIR/pages" ]; then - echo -e "${RED}❌ Failed to clone repository or pages directory not found${NC}" +if [ ! -d "${SYSTEM_OPERATOR_DIR}/pages" ]; then + echo "[setup] ERROR: pages/ directory not found after clone." >&2 exit 1 fi -# Count files -FILE_COUNT=$(find "$SYSTEM_OPERATOR_DIR/pages" -name "*.md" | wc -l) -echo -e "${GREEN}✅ Repository setup complete with $FILE_COUNT markdown files${NC}" - -# Get the absolute path to the project root (we're already in the right place) -PROJECT_ROOT="$(pwd)" - -echo -e "${BLUE}⚙️ Server configuration ready:${NC}" -echo -e " 📄 Config file: $CONFIG_FILE" -echo -e " 🔧 Settings file: $SERVER_SETTINGS" -echo -e " 📚 Documents: $SYSTEM_OPERATOR_DIR/pages ($FILE_COUNT files)" -echo -e " 🌐 Remote KG: https://staging-storage.terraphim.io/thesaurus_Default.json" - -echo -e "${GREEN}🎉 Setup complete!${NC}" -echo -e "${BLUE}To start the server with system operator configuration:${NC}" -echo -e " ${YELLOW}cargo run --bin terraphim_server -- --config $CONFIG_FILE${NC}" - -echo -e "${BLUE}Available roles in this configuration:${NC}" -echo -e " 🔧 System Operator (default) - Uses TerraphimGraph with remote KG" -echo -e " 👷 Engineer - Uses TerraphimGraph with remote KG" -echo -e " 📝 Default - Uses TitleScorer for basic search" - -echo -e "${BLUE}💡 The configuration includes:${NC}" -echo -e " ✅ Remote knowledge graph from staging-storage.terraphim.io" -echo -e " ✅ Local document indexing from GitHub repository" -echo -e " ✅ Read-only document access (safe for production)" -echo -e " ✅ Multiple search backends (Ripgrep + TerraphimGraph)" +PAGE_COUNT=$(find "${SYSTEM_OPERATOR_DIR}/pages" -name "*.md" | wc -l | tr -d ' ') +SYN_COUNT=$(grep -l "^synonyms::" "${SYSTEM_OPERATOR_DIR}/pages/"*.md 2>/dev/null | wc -l | tr -d ' ') + +echo +echo "[setup] Repository ready:" +echo " - Pages: ${PAGE_COUNT} markdown files" +echo " - Synonym entries: ${SYN_COUNT} Terraphim-format files" +echo " - Config: ${CONFIG_FILE}" +echo " - Settings: ${SERVER_SETTINGS}" +echo +echo "[setup] Drive via terraphim_server:" +echo " cargo run --bin terraphim_server -- --config ${CONFIG_FILE}" +echo +echo "[setup] Drive via terraphim-agent CLI (after adding the role to" +echo " ~/.config/terraphim/embedded_config.json, see how-to docs):" +echo " terraphim-agent config reload" +echo " terraphim-agent search --role \"System Operator\" --limit 5 \"RFP\"" +echo +echo "[setup] Available roles in the server config:" +echo " - System Operator (default): TerraphimGraph, Logseq KG, MBSE vocabulary" +echo " - Engineer: TerraphimGraph, same KG, engineering theme" +echo " - Default: TitleScorer, basic text matching" +echo +echo "[setup] Done." diff --git a/terraphim_server/README_SYSTEM_OPERATOR.md b/terraphim_server/README_SYSTEM_OPERATOR.md index 61d00ef31..79dfc7c22 100644 --- a/terraphim_server/README_SYSTEM_OPERATOR.md +++ b/terraphim_server/README_SYSTEM_OPERATOR.md @@ -1,206 +1,171 @@ # Terraphim System Operator Configuration -This document describes how to set up and use the Terraphim server with the **System Operator** role configuration that uses a **remote knowledge graph** and documents from the [terraphim/system-operator](https://github.com/terraphim/system-operator.git) GitHub repository. +This document describes how to run Terraphim with the **System Operator** role, which indexes a Logseq-formatted MBSE vocabulary from [terraphim/system-operator](https://github.com/terraphim/system-operator) and ranks results using Terraphim's knowledge graph. -## 🎯 Overview +The System Operator role is the canonical Logseq-based KG demo. For a local-first companion role that searches personal email and notes, see the [Personal Assistant role how-to](../docs/src/howto/personal-assistant-role.md) -- same engine, different haystacks. + +## Overview The System Operator configuration provides: -- **Remote Knowledge Graph**: Uses pre-built automata from `https://staging-storage.terraphim.io/thesaurus_Default.json` -- **GitHub Document Integration**: Automatically indexes documents from the system-operator repository -- **Advanced Search**: TerraphimGraph ranking with knowledge graph-based relevance scoring -- **System Engineering Focus**: Specialized for MBSE, requirements, architecture, and verification content +- **Logseq knowledge graph** from a public repository: 1,300+ Logseq markdown pages, ~50 of which carry Terraphim-format `synonyms::` lines. +- **GitHub document integration**: `setup_system_operator.sh` clones the repository to a durable local path. +- **TerraphimGraph ranking**: Aho-Corasick automaton built from the synonym files, so queries like `RFP` normalise to `acquisition need` with rank reflecting graph depth. +- **Systems engineering focus**: MBSE vocabulary around requirements, architecture, verification, validation, life cycle concepts. -## 📋 Prerequisites +## Prerequisites - Rust and Cargo installed -- Git for cloning repositories -- Internet connection for remote knowledge graph access -- At least 2GB free disk space for documents - -## 🚀 Quick Start +- Git +- Internet connection for the initial clone and optional remote-thesaurus fetch +- ~200 MB free disk space for the repository -### 1. Setup System Operator Environment +## Quick start -Run the automated setup script: +### 1. Set up the repository ```bash ./scripts/setup_system_operator.sh ``` -This script will: -- Clone the system-operator repository to `/tmp/system_operator` -- Verify markdown files are available -- Display configuration information +This clones `terraphim/system-operator` to `~/.config/terraphim/system_operator` by default (override with `SYSTEM_OPERATOR_DIR=`). The previous default of `/tmp/system_operator` lost the clone on reboot; the new path survives restarts. + +The script prints the page count, synonym-file count, and the commands for the next step. -### 2. Start the Server +### 2. Drive the role via terraphim_server ```bash cargo run --bin terraphim_server -- --config terraphim_server/default/system_operator_config.json ``` -The server will start on `http://127.0.0.1:8000` by default. +The server binds `http://127.0.0.1:8000`. Update the config's `knowledge_graph_local.path` and haystack `location` if you chose a non-default `SYSTEM_OPERATOR_DIR`. + +### 3. Drive the role via the terraphim-agent CLI + +Add this entry to `~/.config/terraphim/embedded_config.json` under `roles`: + +```json +"System Operator": { + "shortname": "SysOps", + "name": "System Operator", + "relevance_function": "terraphim-graph", + "terraphim_it": true, + "theme": "superhero", + "kg": { + "automata_path": null, + "knowledge_graph_local": { + "input_type": "markdown", + "path": "/Users//.config/terraphim/system_operator/pages" + }, + "public": true, + "publish": true + }, + "haystacks": [ + { + "location": "/Users//.config/terraphim/system_operator/pages", + "service": "Ripgrep", + "read_only": true + } + ], + "llm_enabled": false +} +``` -### 3. Test the Configuration +Reload and query: -Run the integration test to verify everything works: +```bash +terraphim-agent config reload +terraphim-agent search --role "System Operator" --limit 5 "RFP" +terraphim-agent validate --role "System Operator" --connectivity "RFP business analysis life cycle model" +``` + +The connectivity check prints the canonical terms each query word matched to -- proof that the KG is actually driving the search. + +### 4. Run the integration test ```bash cargo test --test system_operator_integration_test -- --nocapture ``` -## 📁 Configuration Files +## Configuration files -### Core Configuration -- **`terraphim_server/default/system_operator_config.json`** - Main server configuration -- **`terraphim_server/default/settings_system_operator_server.toml`** - Server settings with S3 profiles +### Core -### Generated Data -- **`/tmp/system_operator/pages/`** - ~1,300 markdown files from GitHub repository -- **Remote KG**: `https://staging-storage.terraphim.io/thesaurus_Default.json` +- `terraphim_server/default/system_operator_config.json` -- server config +- `terraphim_server/default/settings_system_operator_server.toml` -- settings with S3 profiles -## 🔧 Configuration Details +### Generated -### Roles Available +- `~/.config/terraphim/system_operator/pages/` -- 1,300+ markdown files from the repository +- Optional remote KG: `https://staging-storage.terraphim.io/thesaurus_Default.json` -1. **System Operator** (Default) - - **Relevance Function**: `terraphim-graph` - - **Theme**: `superhero` (dark theme) - - **Remote KG**: ✅ Enabled - - **Local Docs**: ✅ `/tmp/system_operator/pages` +## Roles in the server config -2. **Engineer** - - **Relevance Function**: `terraphim-graph` - - **Theme**: `lumen` (light theme) - - **Remote KG**: ✅ Enabled - - **Local Docs**: ✅ `/tmp/system_operator/pages` +| Role | Relevance | Theme | KG | Haystack | +| --- | --- | --- | --- | --- | +| System Operator (default) | TerraphimGraph | superhero | Logseq KG | Ripgrep over pages/ | +| Engineer | TerraphimGraph | lumen | Logseq KG | Ripgrep over pages/ | +| Default | TitleScorer | spacelab | none | Ripgrep over pages/ | -3. **Default** - - **Relevance Function**: `title-scorer` - - **Theme**: `spacelab` - - **Remote KG**: ❌ Disabled - - **Local Docs**: ✅ `/tmp/system_operator/pages` +## Remote thesaurus (optional) -### Remote Knowledge Graph Details +The server config supports a pre-built automaton at `https://staging-storage.terraphim.io/thesaurus_Default.json` for faster cold starts. The `embedded_config` format expects `automata_path: null` (build locally). Pick one path per role -- do not mix. -- **URL**: `https://staging-storage.terraphim.io/thesaurus_Default.json` -- **Type**: Pre-built automata with 1,700+ terms -- **Coverage**: System engineering, MBSE, requirements, architecture -- **Performance**: Fast loading, no local build required +## API usage -## 🔍 API Usage Examples +### Health check -### Health Check ```bash curl http://127.0.0.1:8000/health ``` -### Search with System Operator Role -```bash -curl "http://127.0.0.1:8000/documents/search?q=MBSE&role=System%20Operator&limit=5" -``` +### Search -### Get Configuration ```bash -curl http://127.0.0.1:8000/config +curl "http://127.0.0.1:8000/documents/search?q=MBSE&role=System%20Operator&limit=5" ``` -### Search for Specific Terms -```bash -# Requirements management -curl "http://127.0.0.1:8000/documents/search?q=requirements&role=System%20Operator" - -# Architecture modeling -curl "http://127.0.0.1:8000/documents/search?q=architecture&role=System%20Operator" +Try these MBSE queries to exercise the KG: -# Verification and validation -curl "http://127.0.0.1:8000/documents/search?q=verification&role=System%20Operator" -``` +- `requirements` +- `architecture` +- `verification` +- `RFP` (expands to `acquisition need`) +- `life cycle model` (expands to `life cycle concepts`) -## 🧪 Testing +### Configuration -### Run Integration Tests ```bash -# Run system operator specific tests -cargo test --test system_operator_integration_test -- --nocapture - -# Run all server tests -cargo test -p terraphim_server -- --nocapture +curl http://127.0.0.1:8000/config ``` -### Manual Testing -1. Start the server -2. Open `http://127.0.0.1:8000` in browser -3. Search for terms like "MBSE", "requirements", "system architecture" -4. Verify results are ranked by knowledge graph relevance - -## 📊 Expected Performance - -### Startup Time -- **Remote KG Load**: ~2-3 seconds -- **Document Indexing**: ~5-10 seconds for 1,300+ files -- **Total Startup**: ~15 seconds - -### Search Performance -- **Knowledge Graph Search**: <100ms for most queries -- **Document Count**: 1,300+ markdown files -- **Index Size**: ~50MB in memory - -### Sample Search Results - -When searching for "MBSE": -- Model-Based Systems Engineering documents -- Adoption strategies and best practices -- Tool integration approaches -- Case studies and lessons learned - -## 🛠️ Troubleshooting +## Expected performance -### Common Issues +- Remote KG load: ~2-3 s +- Local document indexing: ~5-10 s for 1,300 files +- Cold start total: ~15 s +- Warm search: <100 ms per query +- In-memory index size: ~50 MB -1. **Repository Clone Failed** - ```bash - # Manual clone - git clone https://github.com/terraphim/system-operator.git /tmp/system_operator - ``` +## Troubleshooting -2. **Remote KG Not Loading** - - Check internet connection - - Verify URL: https://staging-storage.terraphim.io/thesaurus_Default.json - - Check firewall settings +- **Clone failed** -- run `git clone https://github.com/terraphim/system-operator.git ~/.config/terraphim/system_operator` manually. +- **Remote KG not loading** -- check `curl https://staging-storage.terraphim.io/thesaurus_Default.json`; set `automata_path: null` to build locally from `pages/`. +- **No search results** -- confirm `pages/` contains markdown, check server logs, ensure the role uses `terraphim-graph`. +- **Port in use** -- `lsof -i :8000`; start with `--addr 127.0.0.1:8080`. +- **Debug logging** -- `RUST_LOG=debug cargo run --bin terraphim_server -- --config ...`. -3. **No Search Results** - - Ensure documents are in `/tmp/system_operator/pages/` - - Check server logs for indexing errors - - Verify role has `terraphim-graph` relevance function +## Updating documents -4. **Server Won't Start** - ```bash - # Check if port is in use - lsof -i :8000 - - # Use different port - cargo run --bin terraphim_server -- --config terraphim_server/default/system_operator_config.json --addr 127.0.0.1:8080 - ``` - -### Debug Mode ```bash -RUST_LOG=debug cargo run --bin terraphim_server -- --config terraphim_server/default/system_operator_config.json +git -C ~/.config/terraphim/system_operator pull --ff-only origin main +# Restart the server (or run `terraphim-agent config reload`) to re-index. ``` -## 🔄 Updating Documents - -To refresh the system operator documents: - -```bash -cd /tmp/system_operator -git pull origin main -# Restart the server to re-index -``` +## Production deployment -## 🌐 Production Deployment +### Environment variables -### Environment Variables ```bash export TERRAPHIM_SERVER_HOSTNAME="0.0.0.0:8000" export TERRAPHIM_SERVER_API_ENDPOINT="https://your-domain.com/api" @@ -208,33 +173,32 @@ export AWS_ACCESS_KEY_ID="your-access-key" export AWS_SECRET_ACCESS_KEY="your-secret-key" ``` -### Docker Deployment +### Docker + ```bash -# Build with system operator config docker build -t terraphim-system-operator . - -# Run with mounted documents docker run -p 8000:8000 \ - -v /tmp/system_operator:/tmp/system_operator:ro \ + -v ~/.config/terraphim/system_operator:/data/system_operator:ro \ terraphim-system-operator ``` -## 📚 Related Documentation +Adjust the container's config path to match the mount point. -- [Terraphim Configuration Guide](../docs/src/Configuration.md) -- [Knowledge Graph Documentation](../docs/src/kg/) -- [API Reference](../docs/src/API.md) -- [System Operator Repository](https://github.com/terraphim/system-operator) +## Related -## 🤝 Contributing +- [Personal Assistant role how-to](../docs/src/howto/personal-assistant-role.md) -- private, per-user companion pattern +- [Terraphim configuration guide](../docs/src/Configuration.md) +- [Knowledge graph documentation](../docs/src/kg/) +- [API reference](../docs/src/API.md) +- [System Operator repository](https://github.com/terraphim/system-operator) -To improve the system operator configuration: +## Contributing -1. Fork the repository -2. Make changes to configuration files -3. Test with the integration test suite -4. Submit a pull request +1. Fork the repository. +2. Edit configuration files. +3. Run the integration test suite. +4. Open a pull request. -## 📄 License +## Licence -This configuration is part of the Terraphim project and follows the same Apache 2.0 license. +This configuration is part of the Terraphim project and follows the same Apache-2.0 licence.