@ikieaneh/opencode-kit 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/plugins/opencode-kit.js +12 -0
- package/package.json +1 -1
- package/rules/rules.json +20 -0
- package/src/doctor.sh +123 -0
- package/src/init.sh +4 -0
- package/src/postflight.sh +22 -7
- package/src/preflight.sh +61 -45
- package/templates/agents/orchestrator.md +1 -1
- package/templates/contract.json +2 -1
- package/templates/opencode-kit.schema.json +6 -2
|
@@ -171,8 +171,20 @@ export const OpencodeKitPlugin = async ({ client, directory }) => {
|
|
|
171
171
|
config.skills = config.skills || {};
|
|
172
172
|
config.skills.paths = config.skills.paths || [];
|
|
173
173
|
|
|
174
|
+
// Detect if other plugins might conflict with opencode-kit's system prompt
|
|
175
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
176
|
+
const kitIndex = config.plugins.findIndex(p =>
|
|
177
|
+
typeof p === 'string' && p.includes('opencode-kit')
|
|
178
|
+
);
|
|
179
|
+
if (kitIndex > 0) {
|
|
180
|
+
const firstPlugin = config.plugins[0];
|
|
181
|
+
log('warn', `Plugin ordering conflict: opencode-kit should be FIRST, but found '${firstPlugin}' at position 0 and opencode-kit at position ${kitIndex}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
174
185
|
// Register user project skills FIRST (higher priority)
|
|
175
186
|
const userSkillsDir = path.join(projectDir, '.opencode/skills');
|
|
187
|
+
const userSkillsDir = path.join(projectDir, '.opencode/skills');
|
|
176
188
|
if (fs.existsSync(userSkillsDir) && !config.skills.paths.includes(userSkillsDir)) {
|
|
177
189
|
config.skills.paths.push(userSkillsDir);
|
|
178
190
|
log('info', `Registered user skills: ${userSkillsDir}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikieaneh/opencode-kit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Standardized OpenCode orchestration framework — contract-based, rules-enforced, zero-touch agent workflow. Install as plugin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "RizkiRachman",
|
package/rules/rules.json
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
"strict": true,
|
|
4
4
|
"description": "Machine-enforceable rules for OpenCode agents. CRITICAL = BLOCK agent. HIGH = FLAG orchestrator. LOW = advisory.",
|
|
5
5
|
|
|
6
|
+
"required_mcps": {
|
|
7
|
+
"description": "MCPs required for the framework to function. Preflight.sh checks these dynamically.",
|
|
8
|
+
"lean-ctx": {
|
|
9
|
+
"description": "Context persistence — contract storage and retrieval",
|
|
10
|
+
"severity": "required",
|
|
11
|
+
"check_cli": "command -v lean-ctx",
|
|
12
|
+
"check_tool": "lean-ctx ctx_knowledge recall"
|
|
13
|
+
},
|
|
14
|
+
"gitnexus": {
|
|
15
|
+
"description": "Code intelligence — impact analysis before edits",
|
|
16
|
+
"severity": "required",
|
|
17
|
+
"check_cli": "npx --yes gitnexus --version"
|
|
18
|
+
},
|
|
19
|
+
"graphify": {
|
|
20
|
+
"description": "Knowledge graph — codebase exploration",
|
|
21
|
+
"severity": "optional",
|
|
22
|
+
"check_cli": "npx --yes gitnexus analyze --help"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
6
26
|
"state_machine": {
|
|
7
27
|
"transitions": [
|
|
8
28
|
{ "from": "INIT", "to": "PLAN", "require_score": null },
|
package/src/doctor.sh
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# opencode-kit doctor — diagnostic command
|
|
3
|
+
# Checks: MCPs, contract, rules, permissions, git branch, agent configs
|
|
4
|
+
# Usage: bash src/doctor.sh [--json] [--fix]
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
. "$SCRIPT_DIR/platform.sh"
|
|
9
|
+
. "$SCRIPT_DIR/global-config.sh"
|
|
10
|
+
|
|
11
|
+
RULES_FILE=".opencode/rules/rules.json"
|
|
12
|
+
CONTRACT_FILE=".opencode/orchestration/contract.json"
|
|
13
|
+
OPENCODE_JSON="opencode.json"
|
|
14
|
+
|
|
15
|
+
RED='\033[0;31m'
|
|
16
|
+
GREEN='\033[0;32m'
|
|
17
|
+
YELLOW='\033[1;33m'
|
|
18
|
+
CYAN='\033[0;36m'
|
|
19
|
+
NC='\033[0m'
|
|
20
|
+
|
|
21
|
+
ISSUES=0
|
|
22
|
+
mode="${1:-}"
|
|
23
|
+
|
|
24
|
+
echo -e "${CYAN}🔍 opencode-kit doctor${NC}"
|
|
25
|
+
echo ""
|
|
26
|
+
|
|
27
|
+
# === 1. Contract check ===
|
|
28
|
+
echo -e "${CYAN}[CONTRACT]${NC} Checking orchestration contract..."
|
|
29
|
+
if [ ! -f "$CONTRACT_FILE" ]; then
|
|
30
|
+
echo -e " ${RED}❌ contract.json not found — run 'opencode-kit init'${NC}"
|
|
31
|
+
ISSUES=$((ISSUES + 1))
|
|
32
|
+
else
|
|
33
|
+
if [ -n "$PYTHON_CMD" ]; then
|
|
34
|
+
STATE=$($PYTHON_CMD -c "import json; d=json.load(open('$CONTRACT_FILE')); print(d.get('state','?'))" 2>/dev/null || echo "parse_error")
|
|
35
|
+
if [ "$STATE" = "parse_error" ]; then
|
|
36
|
+
echo -e " ${RED}❌ contract.json is malformed JSON${NC}"
|
|
37
|
+
ISSUES=$((ISSUES + 1))
|
|
38
|
+
else
|
|
39
|
+
echo -e " ✅ State: $STATE"
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# === 2. Rules check ===
|
|
45
|
+
echo -e "${CYAN}[RULES]${NC} Checking rules.json..."
|
|
46
|
+
if [ ! -f "$RULES_FILE" ]; then
|
|
47
|
+
echo -e " ${RED}❌ rules.json not found${NC}"
|
|
48
|
+
ISSUES=$((ISSUES + 1))
|
|
49
|
+
else
|
|
50
|
+
if [ -n "$PYTHON_CMD" ]; then
|
|
51
|
+
RULE_COUNT=$($PYTHON_CMD -c "import json; d=json.load(open('$RULES_FILE')); print(len(d.get('rules',[])))" 2>/dev/null || echo "0")
|
|
52
|
+
echo -e " ✅ $RULE_COUNT rules loaded"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# === 3. MCP checks ===
|
|
57
|
+
echo -e "${CYAN}[MCP]${NC} Checking required MCPs..."
|
|
58
|
+
if [ -f "$RULES_FILE" ] && [ -n "$PYTHON_CMD" ]; then
|
|
59
|
+
$PYTHON_CMD -c "
|
|
60
|
+
import json, subprocess, sys
|
|
61
|
+
with open('$RULES_FILE') as f:
|
|
62
|
+
rules = json.load(f)
|
|
63
|
+
mcps = rules.get('required_mcps', {})
|
|
64
|
+
mcps.pop('description', None)
|
|
65
|
+
for name, cfg in mcps.items():
|
|
66
|
+
cli = cfg.get('check_cli', '')
|
|
67
|
+
severity = cfg.get('severity', 'optional')
|
|
68
|
+
result = subprocess.run(cli, shell=True, capture_output=True, timeout=5)
|
|
69
|
+
ok = result.returncode == 0
|
|
70
|
+
if ok:
|
|
71
|
+
print(f' ✅ {name}: available')
|
|
72
|
+
elif severity == 'required':
|
|
73
|
+
print(f' ❌ {name}: MISSING (required)')
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
else:
|
|
76
|
+
print(f' ⚠️ {name}: not detected (optional)')
|
|
77
|
+
" 2>/dev/null || ISSUES=$((ISSUES + 1))
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# === 4. Git branch ===
|
|
81
|
+
echo -e "${CYAN}[GIT]${NC} Checking branch..."
|
|
82
|
+
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
83
|
+
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
84
|
+
echo -e " ${YELLOW}⚠️ On '$BRANCH' — create a feature branch for development${NC}"
|
|
85
|
+
else
|
|
86
|
+
echo -e " ✅ Branch: $BRANCH"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# === 5. Lean-ctx persistence ===
|
|
90
|
+
echo -e "${CYAN}[PERSIST]${NC} Checking persistence..."
|
|
91
|
+
if command -v lean-ctx &>/dev/null; then
|
|
92
|
+
echo -e " ✅ lean-ctx CLI available"
|
|
93
|
+
LEAN_OK=$(lean-ctx ctx_knowledge recall --query "orchestration-contract" &>/dev/null && echo "yes" || echo "no")
|
|
94
|
+
if [ "$LEAN_OK" = "yes" ]; then
|
|
95
|
+
echo -e " ✅ Contract found in lean-ctx"
|
|
96
|
+
else
|
|
97
|
+
echo -e " ⚠️ Contract not in lean-ctx (file fallback active)"
|
|
98
|
+
fi
|
|
99
|
+
else
|
|
100
|
+
echo -e " ⚠️ lean-ctx not detected (file fallback active)"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# === 6. Plugin in opencode.json ===
|
|
104
|
+
echo -e "${CYAN}[PLUGIN]${NC} Checking plugin configuration..."
|
|
105
|
+
if [ -f "$OPENCODE_JSON" ]; then
|
|
106
|
+
if grep -q "@ikieaneh/opencode-kit" "$OPENCODE_JSON" 2>/dev/null; then
|
|
107
|
+
echo -e " ✅ Plugin registered in opencode.json"
|
|
108
|
+
else
|
|
109
|
+
echo -e " ${YELLOW}⚠️ Plugin not found in opencode.json — add to your plugin array${NC}"
|
|
110
|
+
fi
|
|
111
|
+
else
|
|
112
|
+
echo -e " ${YELLOW}⚠️ No opencode.json found${NC}"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# === Summary ===
|
|
116
|
+
echo ""
|
|
117
|
+
if [ "$ISSUES" -eq 0 ]; then
|
|
118
|
+
echo -e "${GREEN}✅ All checks passed. System healthy.${NC}"
|
|
119
|
+
exit 0
|
|
120
|
+
else
|
|
121
|
+
echo -e "${RED}❌ $ISSUES issue(s) found. Review warnings above.${NC}"
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
package/src/init.sh
CHANGED
|
@@ -138,6 +138,10 @@ if [ "$PLUGIN_MODE" = false ]; then
|
|
|
138
138
|
chmod +x .opencode/src/telemetry.sh
|
|
139
139
|
echo " ✅ telemetry.sh (executable)"
|
|
140
140
|
|
|
141
|
+
cp "$KIT_DIR/src/doctor.sh" .opencode/src/doctor.sh
|
|
142
|
+
chmod +x .opencode/src/doctor.sh
|
|
143
|
+
echo " ✅ doctor.sh (executable)"
|
|
144
|
+
|
|
141
145
|
# --- Copy agent templates (pre-flight gates) ---
|
|
142
146
|
for agent in orchestrator planner task-manager code-reviewer learner fixer; do
|
|
143
147
|
if [ -f "$KIT_DIR/templates/agents/$agent.md" ]; then
|
package/src/postflight.sh
CHANGED
|
@@ -11,6 +11,9 @@ CONTRACT_FILE=".opencode/orchestration/contract.json"
|
|
|
11
11
|
STATE_FILE="STATE.md"
|
|
12
12
|
TELEMETRY_DIR=".opencode/telemetry"
|
|
13
13
|
START_TIME_FILE=".opencode/telemetry/.phase_start"
|
|
14
|
+
STATE_BACKUP_DIR=".opencode/state"
|
|
15
|
+
|
|
16
|
+
mkdir -p "$TELEMETRY_DIR" "$STATE_BACKUP_DIR"
|
|
14
17
|
|
|
15
18
|
echo "[opencode-kit] Post-flight: persisting state..."
|
|
16
19
|
|
|
@@ -34,16 +37,28 @@ print(d.get('state','UNKNOWN'))
|
|
|
34
37
|
rm -f "$START_TIME_FILE"
|
|
35
38
|
fi
|
|
36
39
|
|
|
37
|
-
# --- Step 1:
|
|
38
|
-
CURRENT_CONTRACT=$(lean-ctx ctx_knowledge recall --query "$CONTRACT_KEY" 2>/dev/null || cat "$CONTRACT_FILE")
|
|
40
|
+
# --- Step 1: Read contract (try lean-ctx first, fall back to file) ---
|
|
41
|
+
CURRENT_CONTRACT=$(lean-ctx ctx_knowledge recall --query "$CONTRACT_KEY" 2>/dev/null || cat "$CONTRACT_FILE" 2>/dev/null || echo "")
|
|
42
|
+
if [ -z "$CURRENT_CONTRACT" ]; then
|
|
43
|
+
echo " ⚠️ No contract found in lean-ctx or file. Creating new from template..."
|
|
44
|
+
if [ -f "$TEMPLATE_FILE" ]; then
|
|
45
|
+
CURRENT_CONTRACT=$(cat "$TEMPLATE_FILE")
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
39
48
|
|
|
40
|
-
# --- Step 2:
|
|
41
|
-
|
|
49
|
+
# --- Step 2: Persist (try lean-ctx first, fall back to file) ---
|
|
50
|
+
PERSISTED=false
|
|
51
|
+
if lean-ctx ctx_knowledge remember \
|
|
42
52
|
category architecture \
|
|
43
53
|
key "$CONTRACT_KEY" \
|
|
44
|
-
value "$CURRENT_CONTRACT" 2>/dev/null
|
|
45
|
-
echo " ✅ Contract persisted to lean-ctx"
|
|
46
|
-
|
|
54
|
+
value "$CURRENT_CONTRACT" 2>/dev/null; then
|
|
55
|
+
echo " ✅ Contract persisted to lean-ctx"
|
|
56
|
+
PERSISTED=true
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# File fallback: write to .opencode/state/contract.json
|
|
60
|
+
echo "$CURRENT_CONTRACT" > "$STATE_BACKUP_DIR/contract.json"
|
|
61
|
+
echo " ✅ Contract persisted to file: $STATE_BACKUP_DIR/contract.json"
|
|
47
62
|
|
|
48
63
|
# --- Step 3: Sync STATE.md ---
|
|
49
64
|
mkdir -p "$(dirname "$STATE_FILE")"
|
package/src/preflight.sh
CHANGED
|
@@ -33,55 +33,71 @@ if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
|
33
33
|
fi
|
|
34
34
|
echo " ✅ Branch: $BRANCH (safe)"
|
|
35
35
|
|
|
36
|
-
# --- Check 3: MCP Availability ---
|
|
36
|
+
# --- Check 3: MCP Availability (from rules.json) ---
|
|
37
37
|
echo ""
|
|
38
|
-
echo " Checking MCP availability..."
|
|
38
|
+
echo " Checking MCP availability from rules.json..."
|
|
39
39
|
|
|
40
40
|
MCP_FAIL=0
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
42
|
+
if [ -n "$PYTHON_CMD" ] && [ -f "$RULES_FILE" ]; then
|
|
43
|
+
# Parse required_mcps from rules.json
|
|
44
|
+
$PYTHON_CMD -c "
|
|
45
|
+
import json, sys, subprocess, os
|
|
46
|
+
|
|
47
|
+
with open('$RULES_FILE') as f:
|
|
48
|
+
rules = json.load(f)
|
|
49
|
+
|
|
50
|
+
mcps = rules.get('required_mcps', {})
|
|
51
|
+
if not isinstance(mcps, dict) or 'description' in mcps:
|
|
52
|
+
# Skip the meta-description field
|
|
53
|
+
mcps = {k: v for k, v in mcps.items() if k != 'description' and isinstance(v, dict)}
|
|
54
|
+
|
|
55
|
+
if not mcps:
|
|
56
|
+
print(' ℹ️ No required_mcps defined in rules.json — skipping MCP checks')
|
|
57
|
+
sys.exit(0)
|
|
58
|
+
|
|
59
|
+
failures = []
|
|
60
|
+
for name, cfg in mcps.items():
|
|
61
|
+
cli_check = cfg.get('check_cli', '')
|
|
62
|
+
tool_check = cfg.get('check_tool', '')
|
|
63
|
+
severity = cfg.get('severity', 'optional')
|
|
64
|
+
desc = cfg.get('description', name)
|
|
65
|
+
|
|
66
|
+
available = False
|
|
67
|
+
# Try CLI check first
|
|
68
|
+
if cli_check:
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(cli_check, shell=True, capture_output=True, timeout=5)
|
|
71
|
+
if result.returncode == 0:
|
|
72
|
+
available = True
|
|
73
|
+
except:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
# Try tool check as fallback
|
|
77
|
+
if not available and tool_check:
|
|
78
|
+
try:
|
|
79
|
+
result = subprocess.run(tool_check, shell=True, capture_output=True, timeout=5)
|
|
80
|
+
if result.returncode == 0:
|
|
81
|
+
available = True
|
|
82
|
+
except:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
if available:
|
|
86
|
+
print(f' ✅ {name}: available — {desc}')
|
|
87
|
+
elif severity == 'required':
|
|
88
|
+
print(f' ❌ {name}: NOT DETECTED — {desc}')
|
|
89
|
+
failures.append(name)
|
|
90
|
+
else:
|
|
91
|
+
print(f' ⚠️ {name}: not detected — {desc} (optional)')
|
|
92
|
+
|
|
93
|
+
if failures:
|
|
94
|
+
print('')
|
|
95
|
+
for name in failures:
|
|
96
|
+
print(f' → Ensure {name} is configured in opencode.json MCP servers')
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
else:
|
|
99
|
+
sys.exit(0)
|
|
100
|
+
" 2>&1 || MCP_FAIL=1
|
|
85
101
|
fi
|
|
86
102
|
|
|
87
103
|
echo ""
|
|
@@ -94,7 +94,7 @@ Delegate to @code-reviewer. After return → Scoring Pipeline → update contrac
|
|
|
94
94
|
3. **Tier 3 (Verdict)**: ≥70 PASS, 50-69 RETRY, <50 BLOCKED
|
|
95
95
|
|
|
96
96
|
### 5. Verify (loop)
|
|
97
|
-
Run quality gates (
|
|
97
|
+
Run quality gates (format, compile, test, verify)
|
|
98
98
|
If CRITICAL findings → BLOCK, fix, re-review. Max 3 iterations.
|
|
99
99
|
|
|
100
100
|
### 6. Ship
|
package/templates/contract.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"properties": {
|
|
24
24
|
"skills": {
|
|
25
25
|
"type": "array",
|
|
26
|
-
"description": "
|
|
26
|
+
"description": "Skills to load for this agent (e.g., orchestration, scoring, quality gates)",
|
|
27
27
|
"items": { "type": "string" },
|
|
28
28
|
"default": ["orchestration-template", "scoring-pipeline", "verification-before-completion"]
|
|
29
29
|
},
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"properties": {
|
|
38
38
|
"skills": {
|
|
39
39
|
"type": "array",
|
|
40
|
-
"description": "
|
|
40
|
+
"description": "Skills for analysis, requirements gathering, plan writing",
|
|
41
41
|
"items": { "type": "string" },
|
|
42
42
|
"default": ["brainstorming", "writing-plans", "system-analyst"]
|
|
43
43
|
},
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"properties": {
|
|
52
52
|
"skills": {
|
|
53
53
|
"type": "array",
|
|
54
|
+
"description": "Skills for implementation, execution, testing",
|
|
54
55
|
"items": { "type": "string" },
|
|
55
56
|
"default": ["subagent-driven-development", "executing-plans", "test-driven-development"]
|
|
56
57
|
},
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"properties": {
|
|
65
66
|
"skills": {
|
|
66
67
|
"type": "array",
|
|
68
|
+
"description": "Skills for code quality, security, performance review",
|
|
67
69
|
"items": { "type": "string" },
|
|
68
70
|
"default": ["qa-expert", "security-expert", "devops-expert"]
|
|
69
71
|
},
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
"properties": {
|
|
78
80
|
"skills": {
|
|
79
81
|
"type": "array",
|
|
82
|
+
"description": "Skills for verification, quality checks, post-analysis",
|
|
80
83
|
"items": { "type": "string" },
|
|
81
84
|
"default": ["verification-before-completion", "qa-expert"]
|
|
82
85
|
},
|
|
@@ -117,6 +120,7 @@
|
|
|
117
120
|
"properties": {
|
|
118
121
|
"skills": {
|
|
119
122
|
"type": "array",
|
|
123
|
+
"description": "Skills for architecture analysis, simplification, debugging",
|
|
120
124
|
"items": { "type": "string" },
|
|
121
125
|
"default": ["simplify", "systematic-debugging", "system-analyst"]
|
|
122
126
|
},
|